diff --git a/microservices/authorization/migrations/permissions/list/models/payment-stripe.json b/microservices/authorization/migrations/permissions/list/models/payment-stripe.json index 3b579586..50bbb66b 100644 --- a/microservices/authorization/migrations/permissions/list/models/payment-stripe.json +++ b/microservices/authorization/migrations/permissions/list/models/payment-stripe.json @@ -1829,6 +1829,14 @@ "out": { "user": "allow" } + }, + "metadata": { + "in": { + "user": "allow" + }, + "out": { + "user": "allow" + } } }, "createdAt": "2023-05-26T13:01:39.186Z" diff --git a/microservices/payment-stripe/migrations/1753736525000-add-metadata-to-price.ts b/microservices/payment-stripe/migrations/1753736525000-add-metadata-to-price.ts new file mode 100644 index 00000000..0566f940 --- /dev/null +++ b/microservices/payment-stripe/migrations/1753736525000-add-metadata-to-price.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export default class AddMetadataToPrice1753736525000 implements MigrationInterface { + name = 'AddMetadataToPrice1753736525000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "price" + ADD COLUMN "metadata" jsonb DEFAULT null + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "price" + DROP COLUMN "metadata" + `); + } +} diff --git a/microservices/payment-stripe/src/entities/price.ts b/microservices/payment-stripe/src/entities/price.ts index 758ba38a..abdeb199 100644 --- a/microservices/payment-stripe/src/entities/price.ts +++ b/microservices/payment-stripe/src/entities/price.ts @@ -41,6 +41,12 @@ class Price { @IsNumber() unitAmount: number; + @JSONSchema({ + description: 'Metadata for storing custom data like PWYW information', + }) + @Column({ type: 'jsonb', default: null }) + metadata: Record | null; + @IsTypeormDate() @CreateDateColumn() createdAt: Date; diff --git a/microservices/payment-stripe/src/methods/stripe/create-checkout.ts b/microservices/payment-stripe/src/methods/stripe/create-checkout.ts index 9f9ceac2..a4dce7f7 100644 --- a/microservices/payment-stripe/src/methods/stripe/create-checkout.ts +++ b/microservices/payment-stripe/src/methods/stripe/create-checkout.ts @@ -1,5 +1,5 @@ import { Endpoint, IsNullable, IsUndefinable } from '@lomray/microservice-helpers'; -import { IsBoolean, IsNumber, IsString, Length } from 'class-validator'; +import { IsBoolean, IsNumber, IsString, Length, Min } from 'class-validator'; import Stripe from '@services/payment-gateway/stripe'; class CreateCheckoutInput { @@ -21,6 +21,7 @@ class CreateCheckoutInput { isAllowPromoCode?: boolean; @IsNumber() + @Min(0) @IsUndefinable() customAmount?: number; } diff --git a/microservices/payment-stripe/src/services/payment-gateway/abstract.ts b/microservices/payment-stripe/src/services/payment-gateway/abstract.ts index 7014f46b..ca1090af 100644 --- a/microservices/payment-stripe/src/services/payment-gateway/abstract.ts +++ b/microservices/payment-stripe/src/services/payment-gateway/abstract.ts @@ -245,7 +245,7 @@ abstract class Abstract { * Create new price */ public async createPrice(params: IPriceParams, priceId: string = uuid()): Promise { - const { productId, currency, unitAmount, userId } = params; + const { productId, currency, unitAmount, userId, metadata } = params; const price = this.priceRepository.create({ priceId, @@ -253,6 +253,7 @@ abstract class Abstract { userId, currency, unitAmount, + ...(metadata ? { metadata } : {}), }); await this.priceRepository.save(price); diff --git a/microservices/payment-stripe/src/services/payment-gateway/stripe.ts b/microservices/payment-stripe/src/services/payment-gateway/stripe.ts index 39978fc2..78a89062 100644 --- a/microservices/payment-stripe/src/services/payment-gateway/stripe.ts +++ b/microservices/payment-stripe/src/services/payment-gateway/stripe.ts @@ -389,11 +389,31 @@ class Stripe extends Abstract { productId, currency, unitAmount, + metadata, }, id, ); } + /** + * Validate PWYW custom amount against minimum price + */ + private validatePWYWAmount(customAmount: number, price: Price): void { + if (price.metadata?.isPWYW === 'true') { + const minimumPrice = price.metadata?.minimumPrice ? Number(price.metadata.minimumPrice) : 0; + + if (customAmount < minimumPrice) { + Log.error( + `Custom amount ${customAmount} is below minimum price ${minimumPrice} for PWYW show`, + ); + throw new BaseException({ + status: 400, + message: `Amount must be at least $${(minimumPrice / 100).toFixed(2)}`, + }); + } + } + } + /** * Create checkout session and return url to redirect user for payment */ @@ -409,6 +429,11 @@ class Stripe extends Abstract { return null; } + // Validate PWYW minimum price if custom amount is provided + if (customAmount) { + this.validatePWYWAmount(customAmount, price); + } + /* eslint-disable camelcase */ const sessionParams: StripeSdk.Checkout.SessionCreateParams = { mode: 'payment', @@ -423,9 +448,9 @@ class Stripe extends Abstract { sessionParams.line_items = [ { price_data: { - currency: 'usd', + currency: price.currency, product: price.productId, - unit_amount: customAmount * 100, // Convert to cents + unit_amount: customAmount, }, quantity: 1, }, @@ -446,12 +471,12 @@ class Stripe extends Abstract { await this.createTransaction( { type: TransactionType.CREDIT, - amount: customAmount ? customAmount * 100 : price.unitAmount, // Store in cents + amount: customAmount ? customAmount : price.unitAmount, userId, productId: price.productId, entityId: price.product.entityId, status: TransactionStatus.INITIAL, - customAmount: customAmount ? customAmount * 100 : undefined, // Store custom amount in cents + customAmount: customAmount ? customAmount : undefined, // Store custom amount }, id, );