Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,15 @@
"admin": "allow"
}
},
"product": "payment-stripe.Product"
"product": "payment-stripe.Product",
"metadata": {
"in": {
"admin": "allow"
},
"out": {
"admin": "allow"
}
}
},
"createdAt": "2023-05-26T13:01:39.186Z"
},
Expand Down Expand Up @@ -306,7 +314,15 @@
}
},
"customer": "payment-stripe.Customer",
"product": "payment-stripe.Product"
"product": "payment-stripe.Product",
"customAmount": {
"in": {
"admin": "allow"
},
"out": {
"user": "allow"
}
}
},
"createdAt": "2023-05-26T13:01:39.186Z"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export default class AddCustomAmount1753535753000 implements MigrationInterface {
name = 'AddCustomAmount1753535753000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "transaction"
ADD COLUMN "customAmount" integer DEFAULT null
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "transaction"
DROP COLUMN "customAmount"
`);
}
}
9 changes: 9 additions & 0 deletions microservices/payment-stripe/src/entities/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,15 @@ class Transaction {
@IsNumber()
amount: number;

@JSONSchema({
description: 'Custom amount paid by user for PWYW transactions (in cents)',
})
@Column({ type: 'int', default: null })
@IsUndefinable()
@IsNullable()
@IsNumber()
customAmount: number | null;

@JSONSchema({
description: `Sales tax or other, that should be paid to the government by tax collector. Tax included in the
payment intent amount and storing as collected fees amount.`,
Expand Down
6 changes: 5 additions & 1 deletion microservices/payment-stripe/src/methods/price/create.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Endpoint } from '@lomray/microservice-helpers';
import { Type } from 'class-transformer';
import { IsNumber, IsObject, IsString } from 'class-validator';
import { IsNumber, IsObject, IsString, IsOptional } from 'class-validator';
import Price from '@entities/price';
import Stripe from '@services/payment-gateway/stripe';

Expand All @@ -16,6 +16,10 @@ class PriceCreateInput {

@IsNumber()
unitAmount: number;

@IsObject()
@IsOptional()
metadata?: Record<string, string>;
}

class PriceCreateOutput {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Endpoint, IsNullable, IsUndefinable } from '@lomray/microservice-helpers';
import { IsBoolean, IsString, Length } from 'class-validator';
import { IsBoolean, IsNumber, IsString, Length } from 'class-validator';
import Stripe from '@services/payment-gateway/stripe';

class CreateCheckoutInput {
Expand All @@ -19,6 +19,10 @@ class CreateCheckoutInput {
@IsBoolean()
@IsUndefinable()
isAllowPromoCode?: boolean;

@IsNumber()
@IsUndefinable()
customAmount?: number;
}

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

return {
Expand All @@ -46,6 +50,7 @@ const createCheckout = Endpoint.custom(
successUrl,
cancelUrl,
isAllowPromoCode,
customAmount,
}),
};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface IPriceParams {
userId: string;
currency: string;
unitAmount: number;
metadata?: Record<string, string>;
}

export interface ITransactionParams {
Expand All @@ -56,6 +57,7 @@ export interface ITransactionParams {
tax?: number;
fee?: number;
params?: ITransactionEntityParams;
customAmount?: number;
}

export interface IProductParams {
Expand Down
47 changes: 34 additions & 13 deletions microservices/payment-stripe/src/services/payment-gateway/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ interface ICheckoutParams {
successUrl: string;
cancelUrl: string;
isAllowPromoCode?: boolean;
customAmount?: number;
}

interface ICheckoutEvent {
Expand Down Expand Up @@ -369,16 +370,17 @@ class Stripe extends Abstract {
}

/**
* Create Price entity
* Create new price
*/
public async createPrice(params: IPriceParams): Promise<Price> {
const { currency, unitAmount, productId, userId } = params;
const { currency, unitAmount, productId, userId, metadata } = params;

const { id }: StripeSdk.Price = await this.sdk.prices.create({
currency,
product: productId,
// eslint-disable-next-line camelcase
unit_amount: unitAmount,
...(metadata ? { metadata } : {}),
});

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

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

/* eslint-disable camelcase */
const { id, url } = await this.sdk.checkout.sessions.create({
line_items: [
{
price: priceId,
quantity: 1,
},
],
const sessionParams: StripeSdk.Checkout.SessionCreateParams = {
mode: 'payment',
customer: customerId,
success_url: successUrl,
cancel_url: cancelUrl,
allow_promotion_codes: isAllowPromoCode,
});
allow_promotion_codes: isAllowPromoCode && !customAmount, // No promo codes for PWYW
};

if (customAmount) {
// PWYW mode - create custom line item with user-defined amount
sessionParams.line_items = [
{
price_data: {
currency: 'usd',
product: price.productId,
unit_amount: customAmount * 100, // Convert to cents
},
quantity: 1,
},
];
} else {
// Fixed price mode
sessionParams.line_items = [
{
price: priceId,
quantity: 1,
},
];
}

const { id, url } = await this.sdk.checkout.sessions.create(sessionParams);
/* eslint-enable camelcase */

await this.createTransaction(
{
type: TransactionType.CREDIT,
amount: price.unitAmount,
amount: customAmount ? customAmount * 100 : price.unitAmount, // Store in cents
userId,
productId: price.productId,
entityId: price.product.entityId,
status: TransactionStatus.INITIAL,
customAmount: customAmount ? customAmount * 100 : undefined, // Store custom amount in cents
},
id,
);
Expand Down
Loading