Skip to content

Commit 155a3d2

Browse files
committed
atomic logging in stripe module
1 parent c860a03 commit 155a3d2

File tree

3 files changed

+72
-28
lines changed

3 files changed

+72
-28
lines changed

src/api/functions/stripe.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,16 @@ export const createCheckoutSession = async ({
9898
}
9999
return session.url;
100100
};
101+
102+
export const deactivateStripeLink = async ({
103+
linkId,
104+
stripeApiKey,
105+
}: {
106+
linkId: string;
107+
stripeApiKey: string;
108+
}): Promise<void> => {
109+
const stripe = new Stripe(stripeApiKey);
110+
await stripe.paymentLinks.update(linkId, {
111+
active: false,
112+
});
113+
};

src/api/routes/stripe.ts

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,26 @@ import {
22
PutItemCommand,
33
QueryCommand,
44
ScanCommand,
5+
TransactWriteItemsCommand,
6+
TransactWriteItemsCommandInput,
57
} from "@aws-sdk/client-dynamodb";
68
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
79
import { withRoles, withTags } from "api/components/index.js";
8-
import { createAuditLogEntry } from "api/functions/auditLog.js";
10+
import {
11+
buildAuditLogTransactPut,
12+
createAuditLogEntry,
13+
} from "api/functions/auditLog.js";
914
import {
1015
createStripeLink,
16+
deactivateStripeLink,
1117
StripeLinkCreateParams,
1218
} from "api/functions/stripe.js";
1319
import { getSecretValue } from "api/plugins/auth.js";
1420
import { genericConfig } from "common/config.js";
1521
import {
1622
BaseError,
1723
DatabaseFetchError,
24+
DatabaseInsertError,
1825
InternalServerError,
1926
UnauthenticatedError,
2027
} from "common/errors/index.js";
@@ -123,32 +130,53 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
123130
const { url, linkId, priceId, productId } =
124131
await createStripeLink(payload);
125132
const invoiceId = request.body.invoiceId;
126-
const dynamoCommand = new PutItemCommand({
127-
TableName: genericConfig.StripeLinksDynamoTableName,
128-
Item: marshall({
129-
userId: request.username,
130-
linkId,
131-
priceId,
132-
productId,
133-
invoiceId,
134-
url,
135-
amount: request.body.invoiceAmountUsd,
136-
active: true,
137-
createdAt: new Date().toISOString(),
138-
}),
139-
});
140-
const itemPromise = fastify.dynamoClient.send(dynamoCommand);
141-
const logPromise = createAuditLogEntry({
142-
dynamoClient: fastify.dynamoClient,
133+
const logStatement = buildAuditLogTransactPut({
143134
entry: {
144135
module: Modules.STRIPE,
145136
actor: request.username,
146137
target: `Link ${linkId} | Invoice ${invoiceId}`,
147138
message: "Created Stripe payment link",
148139
},
149140
});
150-
await itemPromise;
151-
await logPromise;
141+
const dynamoCommand = new TransactWriteItemsCommand({
142+
TransactItems: [
143+
logStatement,
144+
{
145+
Put: {
146+
TableName: genericConfig.StripeLinksDynamoTableName,
147+
Item: marshall({
148+
userId: request.username,
149+
linkId,
150+
priceId,
151+
productId,
152+
invoiceId,
153+
url,
154+
amount: request.body.invoiceAmountUsd,
155+
active: true,
156+
createdAt: new Date().toISOString(),
157+
}),
158+
},
159+
},
160+
],
161+
});
162+
try {
163+
await fastify.dynamoClient.send(dynamoCommand);
164+
} catch (e) {
165+
await deactivateStripeLink({
166+
stripeApiKey: secretApiConfig.stripe_secret_key as string,
167+
linkId,
168+
});
169+
fastify.log.info(
170+
`Deactivated Stripe link ${linkId} due to error in writing to database.`,
171+
);
172+
if (e instanceof BaseError) {
173+
throw e;
174+
}
175+
fastify.log.error(e);
176+
throw new DatabaseInsertError({
177+
message: "Could not write Stripe link to database.",
178+
});
179+
}
152180
reply.status(201).send({ id: linkId, link: url });
153181
},
154182
);

tests/unit/stripe.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
PutItemCommand,
1212
QueryCommand,
1313
ScanCommand,
14+
TransactWriteItemsCommand,
1415
} from "@aws-sdk/client-dynamodb";
1516
import supertest from "supertest";
1617
import { createJwt } from "./auth.test.js";
1718
import { v4 as uuidv4 } from "uuid";
1819
import { marshall } from "@aws-sdk/util-dynamodb";
20+
import { genericConfig } from "../../src/common/config.js";
1921

2022
const smMock = mockClient(SecretsManagerClient);
2123
const ddbMock = mockClient(DynamoDBClient);
@@ -136,25 +138,26 @@ describe("Test Stripe link creation", async () => {
136138
expect(smMock.calls().length).toEqual(0);
137139
});
138140
test("POST happy path", async () => {
139-
ddbMock.on(PutItemCommand).resolves({});
141+
const invoicePayload = {
142+
invoiceId: "ACM102",
143+
invoiceAmountUsd: 51,
144+
contactName: "Infra User",
145+
contactEmail: "[email protected]",
146+
};
147+
ddbMock.on(TransactWriteItemsCommand).resolvesOnce({}).rejects();
140148
const testJwt = createJwt();
141149
await app.ready();
142150

143151
const response = await supertest(app.server)
144152
.post("/api/v1/stripe/paymentLinks")
145153
.set("authorization", `Bearer ${testJwt}`)
146-
.send({
147-
invoiceId: "ACM102",
148-
invoiceAmountUsd: 51,
149-
contactName: "Infra User",
150-
contactEmail: "[email protected]",
151-
});
154+
.send(invoicePayload);
152155
expect(response.statusCode).toBe(201);
153156
expect(response.body).toStrictEqual({
154157
id: linkId,
155158
link: `https://buy.stripe.com/${linkId}`,
156159
});
157-
expect(ddbMock.calls().length).toEqual(2); // 1 for the audit log
160+
expect(ddbMock.calls().length).toEqual(1);
158161
expect(smMock.calls().length).toEqual(1);
159162
});
160163
test("Unauthenticated GET access (missing token)", async () => {

0 commit comments

Comments
 (0)