Skip to content

Webhook responses for Stripe use-cases #1858

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions apps/stripe/src/__tests__/mocks/app-config-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import { vi } from "vitest";
import { mockStripeWebhookSecret } from "@/__tests__/mocks/stripe-webhook-secret";
import { AppConfigRepo } from "@/modules/app-config/app-config-repo";
import { StripeConfig } from "@/modules/app-config/stripe-config";
import { StripePublishableKey } from "@/modules/stripe/stripe-publishable-key";
import { StripeRestrictedKey } from "@/modules/stripe/stripe-restricted-key";

import { stripePublishableKey } from "./stripe-publishable-key";

const mockedStripeConfig = StripeConfig.create({
id: "config-id",
name: "config-name",
publishableKey: StripePublishableKey.create({
publishableKey: "pk_live_1",
})._unsafeUnwrap(),
publishableKey: stripePublishableKey,
restrictedKey: StripeRestrictedKey.create({ restrictedKey: "rk_live_1" })._unsafeUnwrap(),
webhookSecret: mockStripeWebhookSecret,
})._unsafeUnwrap();
Expand Down
5 changes: 5 additions & 0 deletions apps/stripe/src/__tests__/mocks/stripe-publishable-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { StripePublishableKey } from "@/modules/stripe/stripe-publishable-key";

export const stripePublishableKey = StripePublishableKey.create({
publishableKey: "pk_live_1",
})._unsafeUnwrap();

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,53 @@
import { compose } from "@saleor/apps-shared/compose";
import { captureException } from "@sentry/nextjs";

import { InitializeStripeSessionUseCase } from "@/app/api/saleor/payment-gateway-initialize-session/use-case";
import { paymentGatewayInitializeSessionWebhookDefinition } from "@/app/api/saleor/payment-gateway-initialize-session/webhook-definition";
import { appConfigPersistence } from "@/lib/app-config-persistence";
import { UseCaseGetConfigError, UseCaseMissingConfigError } from "@/lib/errors";
import { withLoggerContext } from "@/lib/logger-context";
import { SaleorApiUrl } from "@/modules/saleor/saleor-api-url";
import {

Check warning on line 8 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L8

Added line #L8 was not covered by tests
MalformedRequestResponse,
UnhandledErrorResponse,
} from "@/modules/saleor/saleor-webhook-responses";

const useCase = new InitializeStripeSessionUseCase({
import { PaymentGatewayInitializeSessionUseCase } from "./use-case";
import { paymentGatewayInitializeSessionWebhookDefinition } from "./webhook-definition";

Check warning on line 14 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L13-L14

Added lines #L13 - L14 were not covered by tests

const useCase = new PaymentGatewayInitializeSessionUseCase({

Check warning on line 16 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L16

Added line #L16 was not covered by tests
appConfigRepo: appConfigPersistence,
});

const handler = paymentGatewayInitializeSessionWebhookDefinition.createHandler(async (req, ctx) => {
/*
* todo create config repo
* todo: should we pass auth data to execute? likely yes
*/
try {
const saleorApiUrlResult = SaleorApiUrl.create({ url: ctx.authData.saleorApiUrl });

Check warning on line 22 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L21-L22

Added lines #L21 - L22 were not covered by tests

const saleorApiUrlResult = SaleorApiUrl.create({ url: ctx.authData.saleorApiUrl });
if (saleorApiUrlResult.isErr()) {
const response = new MalformedRequestResponse();

Check warning on line 25 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L24-L25

Added lines #L24 - L25 were not covered by tests

if (saleorApiUrlResult.isErr()) {
// TODO: maybe we should throw here?
return Response.json(
{
message: "Invalid Saleor API URL",
},
{
status: 400,
},
);
}
captureException(saleorApiUrlResult.error);

Check warning on line 27 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L27

Added line #L27 was not covered by tests

const result = await useCase.execute({
channelId: ctx.payload.sourceObject.channel.id,
appId: ctx.authData.appId,
saleorApiUrl: saleorApiUrlResult.value,
});
return response.getResponse();
}

Check warning on line 30 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L29-L30

Added lines #L29 - L30 were not covered by tests

return result.match(
(result) => {
return Response.json(result);
},
(err) => {
switch (err["constructor"]) {
case UseCaseMissingConfigError:
case UseCaseGetConfigError:
return Response.json(
{
message: err.httpMessage,
},
{
status: err.httpStatusCode,
},
);
const result = await useCase.execute({
channelId: ctx.payload.sourceObject.channel.id,
appId: ctx.authData.appId,
saleorApiUrl: saleorApiUrlResult.value,
});

Check warning on line 36 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L32-L36

Added lines #L32 - L36 were not covered by tests

default: {
captureException(
new Error("Unhandled error in GatewayInitializeSession", {
cause: err,
}),
);
return result.match(
(result) => {
return result.getResponse();
},
(err) => {
return err.getResponse();
},
);
} catch (error) {
captureException(error);
const response = new UnhandledErrorResponse();

Check warning on line 48 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L38-L48

Added lines #L38 - L48 were not covered by tests

return Response.json(
{
message: "Unhandled error",
},
{
status: 500,
},
);
}
}
},
);
return response.getResponse();
}

Check warning on line 51 in apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/stripe/src/app/api/saleor/payment-gateway-initialize-session/route.ts#L50-L51

Added lines #L50 - L51 were not covered by tests
});

export const POST = compose(withLoggerContext, withSpanAttributesAppRouter)(handler);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";

import { stripePublishableKey } from "@/__tests__/mocks/stripe-publishable-key";

import { PaymentGatewayInitializeSessionUseCaseResponses } from "./use-case-response";

describe("PaymentGatewayInitializeSessionUseCaseResponses", () => {
describe("Success", () => {
it("should return fetch API response with status code and message", async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("should return fetch API response with status code and message", async () => {
it("getResponse() returns valid Response with status 200 and formatted 'data' object containing Stripe PK", async () => {

I think Response is more obvious that "fetch API response" (I am editing this comment because I didnt understand it before)

const successResponse = new PaymentGatewayInitializeSessionUseCaseResponses.Success({
pk: stripePublishableKey,
});
const fetchReponse = successResponse.getResponse();

expect(fetchReponse.status).toBe(200);
expect(await fetchReponse.json()).toMatchInlineSnapshot(`
{
"data": {
"stripePublishableKey": "pk_live_1",
},
}
`);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { buildSyncWebhookResponsePayload } from "@saleor/app-sdk/handlers/shared";
import { z } from "zod";

import { SuccessWebhookResponse } from "@/modules/saleor/saleor-webhook-responses";
import { StripePublishableKey } from "@/modules/stripe/stripe-publishable-key";

class Success extends SuccessWebhookResponse {
pk: StripePublishableKey;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you pass it to constructor, better make it readonly or private. Mutation of class member can lead to hard to find bugs and I dont think its our intention


private static ResponseDataSchema = z.object({
stripePublishableKey: z.string(),
});

constructor(args: { pk: StripePublishableKey }) {
super();
this.pk = args.pk;
}

getResponse() {
const typeSafeResponse = buildSyncWebhookResponsePayload<"PAYMENT_GATEWAY_INITIALIZE_SESSION">({
data: Success.ResponseDataSchema.parse({
stripePublishableKey: this.pk.keyValue,
}),
});

return Response.json(typeSafeResponse, { status: this.statusCode });
}
}

export const PaymentGatewayInitializeSessionUseCaseResponses = {
Success,
};

export type PaymentGatewayInitializeSessionUseCaseResponsesType = InstanceType<
(typeof PaymentGatewayInitializeSessionUseCaseResponses)[keyof typeof PaymentGatewayInitializeSessionUseCaseResponses]
>;
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { describe, expect, it, vi } from "vitest";
import { mockedAppConfigRepo } from "@/__tests__/mocks/app-config-repo";
import { mockedSaleorAppId, mockedSaleorChannelId } from "@/__tests__/mocks/constants";
import { mockedSaleorApiUrl } from "@/__tests__/mocks/saleor-api-url";
import { InitializeStripeSessionUseCase } from "@/app/api/saleor/payment-gateway-initialize-session/use-case";
import { UseCaseMissingConfigError } from "@/lib/errors";
import { stripePublishableKey } from "@/__tests__/mocks/stripe-publishable-key";
import { PaymentGatewayInitializeSessionUseCaseResponses } from "@/app/api/saleor/payment-gateway-initialize-session/use-case-response";
import { AppIsNotConfiguredResponse } from "@/modules/saleor/saleor-webhook-responses";

describe("InitializeStripeSessionUseCase", () => {
it('Returns publishable key within "data" object if found in configuration', async () => {
const uc = new InitializeStripeSessionUseCase({
import { PaymentGatewayInitializeSessionUseCase } from "./use-case";

describe("PaymentGatewayInitializeSessionUseCase", () => {
it('Returns Success response with publishable key within "data" object if found in configuration', async () => {
const uc = new PaymentGatewayInitializeSessionUseCase({
appConfigRepo: mockedAppConfigRepo,
});

Expand All @@ -19,19 +22,25 @@ describe("InitializeStripeSessionUseCase", () => {
appId: mockedSaleorAppId,
});

expect(responsePayload._unsafeUnwrap()).toStrictEqual({
expect(responsePayload._unsafeUnwrap()).toBeInstanceOf(
PaymentGatewayInitializeSessionUseCaseResponses.Success,
);

const jsonResponse = await responsePayload._unsafeUnwrap().getResponse().json();

expect(jsonResponse).toStrictEqual({
data: {
stripePublishableKey: "pk_live_1",
stripePublishableKey: stripePublishableKey.keyValue,
},
});
});

it("Returns AppNotConfiguredError if config not found for specified channel", async () => {
it("Returns MissingConfigErrorResponse if config not found for specified channel", async () => {
const spy = vi
.spyOn(mockedAppConfigRepo, "getStripeConfig")
.mockImplementationOnce(async () => ok(null));

const uc = new InitializeStripeSessionUseCase({
const uc = new PaymentGatewayInitializeSessionUseCase({
appConfigRepo: mockedAppConfigRepo,
});

Expand All @@ -43,7 +52,7 @@ describe("InitializeStripeSessionUseCase", () => {

const err = responsePayload._unsafeUnwrapErr();

expect(err).toBeInstanceOf(UseCaseMissingConfigError);
expect(err).toBeInstanceOf(AppIsNotConfiguredResponse);
expect(spy).toHaveBeenCalledOnce();
});
});
Loading
Loading