From e6cf53ad59b62ed95b9365d4a91c32e7ae6d4d2a Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sat, 14 Sep 2024 18:52:45 +1200 Subject: [PATCH 1/2] Add getLodgePrices endpoint to fetch lodge prices --- client/src/models/__generated__/schema.d.ts | 29 ++++++++ .../business-layer/services/StripeService.ts | 22 ++++++ server/src/middleware/__generated__/routes.ts | 44 ++++++++++++ .../src/middleware/__generated__/swagger.json | 71 +++++++++++++++++++ .../controllers/PaymentController.ts | 56 ++++++++++++++- .../response-models/PaymentResponse.ts | 17 ++++- 6 files changed, 237 insertions(+), 2 deletions(-) diff --git a/client/src/models/__generated__/schema.d.ts b/client/src/models/__generated__/schema.d.ts index 1568af09d..9b305014b 100644 --- a/client/src/models/__generated__/schema.d.ts +++ b/client/src/models/__generated__/schema.d.ts @@ -32,6 +32,10 @@ export interface paths { /** @description Fetches the prices of the membership products from Stripe. */ get: operations["GetMembershipPrices"]; }; + "/payment/lodge_prices": { + /** @description Fetches the prices of the lodge products from Stripe. */ + get: operations["GetLodgePrices"]; + }; "/payment/checkout_status": { /** @description Fetches the details of a checkout session based on a stripe checkout session id. */ get: operations["GetCheckoutSessionDetails"]; @@ -246,6 +250,20 @@ export interface components { }[]; }; /** @enum {string} */ + LodgePricingTypeValues: "single_friday_or_saturday" | "normal"; + LodgeStripeProductResponse: { + error?: string; + message?: string; + data?: { + originalPrice?: string; + displayPrice: string; + discount: boolean; + description?: string; + name: components["schemas"]["LodgePricingTypeValues"]; + productId: string; + }[]; + }; + /** @enum {string} */ "stripe.Stripe.Checkout.Session.Status": "complete" | "expired" | "open"; /** @description Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format. */ "stripe.Stripe.Metadata": { @@ -741,6 +759,17 @@ export interface operations { }; }; }; + /** @description Fetches the prices of the lodge products from Stripe. */ + GetLodgePrices: { + responses: { + /** @description The prices of the lodge products. */ + 200: { + content: { + "application/json": components["schemas"]["LodgeStripeProductResponse"]; + }; + }; + }; + }; /** @description Fetches the details of a checkout session based on a stripe checkout session id. */ GetCheckoutSessionDetails: { parameters: { diff --git a/server/src/business-layer/services/StripeService.ts b/server/src/business-layer/services/StripeService.ts index 8f7e9c504..02c333dc0 100644 --- a/server/src/business-layer/services/StripeService.ts +++ b/server/src/business-layer/services/StripeService.ts @@ -377,6 +377,28 @@ export default class StripeService { } } + /** Fetch all active products from Stripe + * @returns lodgeProducts - An array of active lodge products from Stripe + */ + public async getActiveLodgeProducts() { + try { + const products = await stripe.products.list({ + active: true, + expand: ["data.default_price"] + }) + // Filter products with the required metadata + const lodgeProducts = products.data.filter( + (product) => + product.metadata[MEMBERSHIP_PRODUCT_TYPE_KEY] === + ProductTypeValues.BOOKING + ) + return lodgeProducts + } catch (error) { + console.error("Error fetching Stripe products:", error) + throw error + } + } + /** * Promotes a user from guest to member status. * @param uid The user ID to promote to a member. diff --git a/server/src/middleware/__generated__/routes.ts b/server/src/middleware/__generated__/routes.ts index 75dd6b398..d46b6ddd2 100644 --- a/server/src/middleware/__generated__/routes.ts +++ b/server/src/middleware/__generated__/routes.ts @@ -115,6 +115,21 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "LodgePricingTypeValues": { + "dataType": "refEnum", + "enums": ["single_friday_or_saturday","normal"], + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "LodgeStripeProductResponse": { + "dataType": "refObject", + "properties": { + "error": {"dataType":"string"}, + "message": {"dataType":"string"}, + "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"originalPrice":{"dataType":"string"},"displayPrice":{"dataType":"string","required":true},"discount":{"dataType":"boolean","required":true},"description":{"dataType":"string"},"name":{"ref":"LodgePricingTypeValues","required":true},"productId":{"dataType":"string","required":true}}}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "stripe.Stripe.Checkout.Session.Status": { "dataType": "refAlias", "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["complete"]},{"dataType":"enum","enums":["expired"]},{"dataType":"enum","enums":["open"]}],"validators":{}}, @@ -710,6 +725,35 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/payment/lodge_prices', + ...(fetchMiddlewares(PaymentController)), + ...(fetchMiddlewares(PaymentController.prototype.getLodgePrices)), + + function PaymentController_getLodgePrices(request: ExRequest, response: ExResponse, next: any) { + const args: Record = { + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new PaymentController(); + + templateService.apiHandler({ + methodName: 'getLodgePrices', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/payment/checkout_status', authenticateMiddleware([{"jwt":[]}]), ...(fetchMiddlewares(PaymentController)), diff --git a/server/src/middleware/__generated__/swagger.json b/server/src/middleware/__generated__/swagger.json index 27405b340..353b3488c 100644 --- a/server/src/middleware/__generated__/swagger.json +++ b/server/src/middleware/__generated__/swagger.json @@ -272,6 +272,57 @@ "type": "object", "additionalProperties": false }, + "LodgePricingTypeValues": { + "enum": [ + "single_friday_or_saturday", + "normal" + ], + "type": "string" + }, + "LodgeStripeProductResponse": { + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "data": { + "items": { + "properties": { + "originalPrice": { + "type": "string" + }, + "displayPrice": { + "type": "string" + }, + "discount": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/LodgePricingTypeValues" + }, + "productId": { + "type": "string" + } + }, + "required": [ + "displayPrice", + "discount", + "name", + "productId" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object", + "additionalProperties": false + }, "stripe.Stripe.Checkout.Session.Status": { "type": "string", "enum": [ @@ -1516,6 +1567,26 @@ "parameters": [] } }, + "/payment/lodge_prices": { + "get": { + "operationId": "GetLodgePrices", + "responses": { + "200": { + "description": "The prices of the lodge products.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LodgeStripeProductResponse" + } + } + } + } + }, + "description": "Fetches the prices of the lodge products from Stripe.", + "security": [], + "parameters": [] + } + }, "/payment/checkout_status": { "get": { "operationId": "GetCheckoutSessionDetails", diff --git a/server/src/service-layer/controllers/PaymentController.ts b/server/src/service-layer/controllers/PaymentController.ts index 9b81acdf8..3c8696ff4 100644 --- a/server/src/service-layer/controllers/PaymentController.ts +++ b/server/src/service-layer/controllers/PaymentController.ts @@ -10,7 +10,8 @@ import { import { MembershipTypeValues, MEMBERSHIP_TYPE_KEY, - LODGE_PRICING_TYPE_KEY + LODGE_PRICING_TYPE_KEY, + LodgePricingTypeValues } from "business-layer/utils/StripeProductMetadata" import { UTCDateToDdMmYyyy, @@ -27,6 +28,7 @@ import { } from "service-layer/request-models/UserRequests" import { BookingPaymentResponse, + LodgeStripeProductResponse, MembershipPaymentResponse, MembershipStripeProductResponse } from "service-layer/response-models/PaymentResponse" @@ -107,6 +109,58 @@ export class PaymentController extends Controller { } } + /** + * Fetches the prices of the lodge products from Stripe. + * @returns The prices of the lodge products. + */ + @Get("lodge_prices") + public async getLodgePrices(): Promise { + const stripeService = new StripeService() + try { + const lodgeProducts = await stripeService.getActiveMembershipProducts() + // Maps the products to the required response type MembershipStripeProductResponse in PaymentResponse + + const productsValues = lodgeProducts.map((product) => { + // Checks the membership type of the product + const lodgeType = product.metadata[ + LODGE_PRICING_TYPE_KEY + ] as LodgePricingTypeValues + + let name: LodgePricingTypeValues + + switch (lodgeType) { + case LodgePricingTypeValues.SingleFridayOrSaturday: { + name = LodgePricingTypeValues.SingleFridayOrSaturday + break + } + case LodgePricingTypeValues.Normal: { + name = LodgePricingTypeValues.Normal + break + } + } + + return { + productId: product.id, + name, + description: product.description, + discount: product.metadata.discount === "true", + displayPrice: ( + Number( + (product.default_price as Stripe.Price).unit_amount_decimal + ) / 100 + ).toString(), + originalPrice: product.metadata.original_price + } + }) + this.setStatus(200) + return { data: productsValues } + } catch (error) { + console.error(error) + this.setStatus(500) + return { error: "Error fetching active Stripe products" } + } + } + /** * Fetches the details of a checkout session based on a stripe checkout session id. * @param sessionId The id of the stripe checkout session to fetch. diff --git a/server/src/service-layer/response-models/PaymentResponse.ts b/server/src/service-layer/response-models/PaymentResponse.ts index 7b083e7e3..b7a9aad11 100644 --- a/server/src/service-layer/response-models/PaymentResponse.ts +++ b/server/src/service-layer/response-models/PaymentResponse.ts @@ -1,4 +1,7 @@ -import { MembershipTypeValues } from "business-layer/utils/StripeProductMetadata" +import { + LodgePricingTypeValues, + MembershipTypeValues +} from "business-layer/utils/StripeProductMetadata" import { CommonResponse } from "./CommonResponse" import { Timestamp } from "firebase-admin/firestore" @@ -31,6 +34,18 @@ export interface MembershipStripeProductResponse extends CommonResponse { }[] } +// Make a data shape matching to the expected response from Stripe API +export interface LodgeStripeProductResponse extends CommonResponse { + data?: { + productId: string + name: LodgePricingTypeValues + description?: string + discount: boolean + displayPrice: string + originalPrice?: string + }[] +} + export interface AvailableDatesResponse extends CommonResponse { data?: AvailableDates[] } From e3ca8012eb4d34dcf1f5cac271319070434b654c Mon Sep 17 00:00:00 2001 From: jeffplays2005 Date: Sat, 14 Sep 2024 22:05:17 +1200 Subject: [PATCH 2/2] Update endpoint to use correct methods and update stripeservice for correct variable names Used the wrong method for endpoint so fixed that. --- server/src/business-layer/services/StripeService.ts | 7 +++---- server/src/service-layer/controllers/PaymentController.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/server/src/business-layer/services/StripeService.ts b/server/src/business-layer/services/StripeService.ts index 02c333dc0..20f065e81 100644 --- a/server/src/business-layer/services/StripeService.ts +++ b/server/src/business-layer/services/StripeService.ts @@ -1,4 +1,5 @@ import { + LODGE_PRICING_TYPE_KEY, MEMBERSHIP_PRODUCT_TYPE_KEY, ProductTypeValues, USER_FIREBASE_EMAIL_KEY, @@ -387,10 +388,8 @@ export default class StripeService { expand: ["data.default_price"] }) // Filter products with the required metadata - const lodgeProducts = products.data.filter( - (product) => - product.metadata[MEMBERSHIP_PRODUCT_TYPE_KEY] === - ProductTypeValues.BOOKING + const lodgeProducts = products.data.filter((product) => + Object.keys(product.metadata).includes(LODGE_PRICING_TYPE_KEY) ) return lodgeProducts } catch (error) { diff --git a/server/src/service-layer/controllers/PaymentController.ts b/server/src/service-layer/controllers/PaymentController.ts index 3c8696ff4..9d2bf758c 100644 --- a/server/src/service-layer/controllers/PaymentController.ts +++ b/server/src/service-layer/controllers/PaymentController.ts @@ -117,7 +117,7 @@ export class PaymentController extends Controller { public async getLodgePrices(): Promise { const stripeService = new StripeService() try { - const lodgeProducts = await stripeService.getActiveMembershipProducts() + const lodgeProducts = await stripeService.getActiveLodgeProducts() // Maps the products to the required response type MembershipStripeProductResponse in PaymentResponse const productsValues = lodgeProducts.map((product) => {