Skip to content

Commit c86f2f3

Browse files
authored
feat(payment-stripe): add support for custom payment amounts (PWYW) (#209)
1 parent 28e1036 commit c86f2f3

File tree

7 files changed

+94
-18
lines changed

7 files changed

+94
-18
lines changed

microservices/authorization/migrations/permissions/list/models/payment-stripe.json

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,15 @@
6363
"admin": "allow"
6464
}
6565
},
66-
"product": "payment-stripe.Product"
66+
"product": "payment-stripe.Product",
67+
"metadata": {
68+
"in": {
69+
"admin": "allow"
70+
},
71+
"out": {
72+
"admin": "allow"
73+
}
74+
}
6775
},
6876
"createdAt": "2023-05-26T13:01:39.186Z"
6977
},
@@ -306,7 +314,15 @@
306314
}
307315
},
308316
"customer": "payment-stripe.Customer",
309-
"product": "payment-stripe.Product"
317+
"product": "payment-stripe.Product",
318+
"customAmount": {
319+
"in": {
320+
"admin": "allow"
321+
},
322+
"out": {
323+
"user": "allow"
324+
}
325+
}
310326
},
311327
"createdAt": "2023-05-26T13:01:39.186Z"
312328
},
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export default class AddCustomAmount1753535753000 implements MigrationInterface {
4+
name = 'AddCustomAmount1753535753000';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
ALTER TABLE "transaction"
9+
ADD COLUMN "customAmount" integer DEFAULT null
10+
`);
11+
}
12+
13+
public async down(queryRunner: QueryRunner): Promise<void> {
14+
await queryRunner.query(`
15+
ALTER TABLE "transaction"
16+
DROP COLUMN "customAmount"
17+
`);
18+
}
19+
}

microservices/payment-stripe/src/entities/transaction.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,15 @@ class Transaction {
215215
@IsNumber()
216216
amount: number;
217217

218+
@JSONSchema({
219+
description: 'Custom amount paid by user for PWYW transactions (in cents)',
220+
})
221+
@Column({ type: 'int', default: null })
222+
@IsUndefinable()
223+
@IsNullable()
224+
@IsNumber()
225+
customAmount: number | null;
226+
218227
@JSONSchema({
219228
description: `Sales tax or other, that should be paid to the government by tax collector. Tax included in the
220229
payment intent amount and storing as collected fees amount.`,

microservices/payment-stripe/src/methods/price/create.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Endpoint } from '@lomray/microservice-helpers';
22
import { Type } from 'class-transformer';
3-
import { IsNumber, IsObject, IsString } from 'class-validator';
3+
import { IsNumber, IsObject, IsString, IsOptional } from 'class-validator';
44
import Price from '@entities/price';
55
import Stripe from '@services/payment-gateway/stripe';
66

@@ -16,6 +16,10 @@ class PriceCreateInput {
1616

1717
@IsNumber()
1818
unitAmount: number;
19+
20+
@IsObject()
21+
@IsOptional()
22+
metadata?: Record<string, string>;
1923
}
2024

2125
class PriceCreateOutput {

microservices/payment-stripe/src/methods/stripe/create-checkout.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Endpoint, IsNullable, IsUndefinable } from '@lomray/microservice-helpers';
2-
import { IsBoolean, IsString, Length } from 'class-validator';
2+
import { IsBoolean, IsNumber, IsString, Length } from 'class-validator';
33
import Stripe from '@services/payment-gateway/stripe';
44

55
class CreateCheckoutInput {
@@ -19,6 +19,10 @@ class CreateCheckoutInput {
1919
@IsBoolean()
2020
@IsUndefinable()
2121
isAllowPromoCode?: boolean;
22+
23+
@IsNumber()
24+
@IsUndefinable()
25+
customAmount?: number;
2226
}
2327

2428
class CreateCheckoutOutput {
@@ -36,7 +40,7 @@ const createCheckout = Endpoint.custom(
3640
output: CreateCheckoutOutput,
3741
description: 'Setup intent and return client secret key',
3842
}),
39-
async ({ priceId, successUrl, cancelUrl, userId, isAllowPromoCode }) => {
43+
async ({ priceId, successUrl, cancelUrl, userId, isAllowPromoCode, customAmount }) => {
4044
const service = await Stripe.init();
4145

4246
return {
@@ -46,6 +50,7 @@ const createCheckout = Endpoint.custom(
4650
successUrl,
4751
cancelUrl,
4852
isAllowPromoCode,
53+
customAmount,
4954
}),
5055
};
5156
},

microservices/payment-stripe/src/services/payment-gateway/abstract.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface IPriceParams {
4141
userId: string;
4242
currency: string;
4343
unitAmount: number;
44+
metadata?: Record<string, string>;
4445
}
4546

4647
export interface ITransactionParams {
@@ -56,6 +57,7 @@ export interface ITransactionParams {
5657
tax?: number;
5758
fee?: number;
5859
params?: ITransactionEntityParams;
60+
customAmount?: number;
5961
}
6062

6163
export interface IProductParams {

microservices/payment-stripe/src/services/payment-gateway/stripe.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ interface ICheckoutParams {
9999
successUrl: string;
100100
cancelUrl: string;
101101
isAllowPromoCode?: boolean;
102+
customAmount?: number;
102103
}
103104

104105
interface ICheckoutEvent {
@@ -369,16 +370,17 @@ class Stripe extends Abstract {
369370
}
370371

371372
/**
372-
* Create Price entity
373+
* Create new price
373374
*/
374375
public async createPrice(params: IPriceParams): Promise<Price> {
375-
const { currency, unitAmount, productId, userId } = params;
376+
const { currency, unitAmount, productId, userId, metadata } = params;
376377

377378
const { id }: StripeSdk.Price = await this.sdk.prices.create({
378379
currency,
379380
product: productId,
380381
// eslint-disable-next-line camelcase
381382
unit_amount: unitAmount,
383+
...(metadata ? { metadata } : {}),
382384
});
383385

384386
return super.createPrice(
@@ -396,7 +398,7 @@ class Stripe extends Abstract {
396398
* Create checkout session and return url to redirect user for payment
397399
*/
398400
public async createCheckout(params: ICheckoutParams): Promise<string | null> {
399-
const { priceId, userId, successUrl, cancelUrl, isAllowPromoCode } = params;
401+
const { priceId, userId, successUrl, cancelUrl, isAllowPromoCode, customAmount } = params;
400402

401403
const { customerId } = await super.getCustomer(userId);
402404
const price = await this.priceRepository.findOne({ priceId }, { relations: ['product'] });
@@ -408,29 +410,48 @@ class Stripe extends Abstract {
408410
}
409411

410412
/* eslint-disable camelcase */
411-
const { id, url } = await this.sdk.checkout.sessions.create({
412-
line_items: [
413-
{
414-
price: priceId,
415-
quantity: 1,
416-
},
417-
],
413+
const sessionParams: StripeSdk.Checkout.SessionCreateParams = {
418414
mode: 'payment',
419415
customer: customerId,
420416
success_url: successUrl,
421417
cancel_url: cancelUrl,
422-
allow_promotion_codes: isAllowPromoCode,
423-
});
418+
allow_promotion_codes: isAllowPromoCode && !customAmount, // No promo codes for PWYW
419+
};
420+
421+
if (customAmount) {
422+
// PWYW mode - create custom line item with user-defined amount
423+
sessionParams.line_items = [
424+
{
425+
price_data: {
426+
currency: 'usd',
427+
product: price.productId,
428+
unit_amount: customAmount * 100, // Convert to cents
429+
},
430+
quantity: 1,
431+
},
432+
];
433+
} else {
434+
// Fixed price mode
435+
sessionParams.line_items = [
436+
{
437+
price: priceId,
438+
quantity: 1,
439+
},
440+
];
441+
}
442+
443+
const { id, url } = await this.sdk.checkout.sessions.create(sessionParams);
424444
/* eslint-enable camelcase */
425445

426446
await this.createTransaction(
427447
{
428448
type: TransactionType.CREDIT,
429-
amount: price.unitAmount,
449+
amount: customAmount ? customAmount * 100 : price.unitAmount, // Store in cents
430450
userId,
431451
productId: price.productId,
432452
entityId: price.product.entityId,
433453
status: TransactionStatus.INITIAL,
454+
customAmount: customAmount ? customAmount * 100 : undefined, // Store custom amount in cents
434455
},
435456
id,
436457
);

0 commit comments

Comments
 (0)