Skip to content

Commit dbf4bdc

Browse files
authored
feat(payment-stripe): add custom unit amount support for prices, upgrade stripe versions (#212)
* feat(payment-stripe): add custom unit amount support for prices, upgrade stripe versions * fix(payment-stripe): rename `custom_unit_amount` to `customUnitAmount` in permissions model * fix(payment-stripe): update permissions for `customUnitAmount` to allow admin access only
1 parent c33e7b6 commit dbf4bdc

File tree

13 files changed

+147
-144
lines changed

13 files changed

+147
-144
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
MS_INIT_CONFIGS='[{"microservice":"*","type":"db","params":{"host":"db","port":5432,"username":"postgres","password":"example"}},{"microservice":"authentication","type":"config","params":{"jwtOptions":{"secretKey":"DemoSecretKey"}}},{"microservice":"authorization","type":"config","params":{}},{"microservice":"content","type":"config","params":{}},{"microservice":"gateway","type":"config","params":{"corsOptions":{},"webhookUrl":"/webhook/"}},{"microservice":"users","type":"config","params":{"removedAccountRestoreTime":0}},{"microservice":"notification","type":"config","params":{"defaultEmailFrom":"[email protected]","transportOptions":{"host":"smtp.ethereal.email","port":587,"secure":false,"auth":{"user":"generated ethereal user","pass":"generated ethereal password"}}}},{"microservice":"files","type":"config","params":{"imageProcessingConfig":{"thumbnails":[{"name":"thumbnail","options":{"width":75}},{"name":"small","options":{"width":150}},{"name":"medium","options":{"width":300}},{"name":"large","options":{"width":600}},{"name":"extra-large","options":{"width":1200}}],"outputOptions":{"jpeg":{"quality":80,"mozjpeg":true},"png":{"quality":80},"webp":{"quality":80}},"isWebp":true}}},{"microservice":"payment-stripe","type":"config","params":{"paymentMethods":["bancontact","card"],"apiKey":"your test key from stripe or other service","config":{"apiVersion":"2022-11-15"},"payoutCoeff":0.3,"webhookKeys":{"connect":"your test webhook key from stripe or other service for connect account"},"fees":{"stablePaymentUnit":30,"stableDisputeFeeUnit":1500,"paymentPercent":2.9,"instantPayoutPercent":1},"duplicatedCardsUsage":"reject"}}]'
1+
MS_INIT_CONFIGS='[{"microservice":"*","type":"db","params":{"host":"db","port":5432,"username":"postgres","password":"example"}},{"microservice":"authentication","type":"config","params":{"jwtOptions":{"secretKey":"DemoSecretKey"}}},{"microservice":"authorization","type":"config","params":{}},{"microservice":"content","type":"config","params":{}},{"microservice":"gateway","type":"config","params":{"corsOptions":{},"webhookUrl":"/webhook/"}},{"microservice":"users","type":"config","params":{"removedAccountRestoreTime":0}},{"microservice":"notification","type":"config","params":{"defaultEmailFrom":"[email protected]","transportOptions":{"host":"smtp.ethereal.email","port":587,"secure":false,"auth":{"user":"generated ethereal user","pass":"generated ethereal password"}}}},{"microservice":"files","type":"config","params":{"imageProcessingConfig":{"thumbnails":[{"name":"thumbnail","options":{"width":75}},{"name":"small","options":{"width":150}},{"name":"medium","options":{"width":300}},{"name":"large","options":{"width":600}},{"name":"extra-large","options":{"width":1200}}],"outputOptions":{"jpeg":{"quality":80,"mozjpeg":true},"png":{"quality":80},"webp":{"quality":80}},"isWebp":true}}},{"microservice":"payment-stripe","type":"config","params":{"paymentMethods":["bancontact","card"],"apiKey":"your test key from stripe or other service","config":{"apiVersion":"2025-06-30.basil"},"payoutCoeff":0.3,"webhookKeys":{"connect":"your test webhook key from stripe or other service for connect account"},"fees":{"stablePaymentUnit":30,"stableDisputeFeeUnit":1500,"paymentPercent":2.9,"instantPayoutPercent":1},"duplicatedCardsUsage":"reject"}}]'
22
MS_INIT_MIDDLEWARES='[{"target":"gateway","targetMethod":"*","sender":"authentication","senderMethod":"token.identify","type":"request","order":10,"description":"Validate JWT token and add payload with token info.","params":{"type":"request","isRequired":true,"maxValueSize":100,"exclude":["users.user.sign-in","users.identity-provider.sign-in","authentication.cookies.remove","authentication.token.renew"],"strategy":"transform","convertResult":{"payload.authentication.tokenId":"$middleware.tokenId","payload.authentication.userId":"$middleware.userId","payload.authentication.isAuth":"$middleware.isAuth","payload.authentication.provider":"$middleware.provider"}}},{"target":"gateway","targetMethod":"*","sender":"authorization","senderMethod":"endpoint.enforce","type":"request","order":20,"description":"Whether the user is allowed to call the method. Filter method input params.","params":{"type":"request","isRequired":true,"isCleanResult":true,"strategy":"transform","reqParams":{"isInternal":true},"convertParams":{"userId":"$task.params.payload.authentication.userId","method":"$task.method","filterInput":"$task.params"},"convertResult":{".":"$middleware.filteredInput","payload":"$task.params.payload","payload.authorization.isAllow":"$middleware.isAllow","payload.authorization.roles":"$middleware.roles","payload.authorization.filter":"$middleware.filters"}}},{"target":"gateway","targetMethod":"*","sender":"authorization","senderMethod":"endpoint.filter","type":"response","order":20,"description":"Filter microservice response fields.","params":{"type":"response","isRequired":true,"isCleanResult":true,"strategy":"transform","reqParams":{"isInternal":true},"convertParams":{"type":"out","userId":"$task.params.payload.authentication.userId","method":"$task.method","filterInput":"$result"},"convertResult":{".":"$middleware.filtered"}}},{"target":"users","targetMethod":"identity-provider.sign-in","sender":"authentication","senderMethod":"token.create","type":"response","order":10,"description":"Create JWT auth tokens and attach to response after successful sign in.","params":{"type":"response","isRequired":true,"strategy":"transform","extraRequests":[{"key":"rolesResp","method":"authorization.user-role.view","params":{"userId":"<%= result.user.id %>"}}],"convertParams":{"type":"jwt","userId":"$result.user.id","params":"$task.params.payload.headers.user-info","jwtPayload.roles":"$rolesResp.roles","returnType":"<%= _.get(task, \"params.payload.headers.user-info.authType\", \"cookies\") %>"},"convertResult":{"payload.cookies":"$middleware.payload.cookies","tokens.access":"$middleware.access","tokens.refresh":"$middleware.refresh"}}},{"target":"users","targetMethod":"user.sign-in","sender":"authentication","senderMethod":"token.create","type":"response","order":10,"description":"Create JWT auth tokens and attach to response after successful sign in.","params":{"type":"response","isRequired":true,"strategy":"transform","extraRequests":[{"key":"rolesResp","method":"authorization.user-role.view","params":{"userId":"<%= result.user.id %>"}}],"convertParams":{"type":"jwt","userId":"$result.user.id","params":"$task.params.payload.headers.user-info","jwtPayload.roles":"$rolesResp.roles","returnType":"<%= _.get(task, \"params.payload.headers.user-info.authType\", \"cookies\") %>"},"convertResult":{"payload.cookies":"$middleware.payload.cookies","tokens.access":"$middleware.access","tokens.refresh":"$middleware.refresh"}}},{"target":"users","targetMethod":"user.sign-up","sender":"authentication","senderMethod":"token.create","type":"response","order":10,"description":"Create JWT auth tokens and attach to response after successful sign up.","params":{"type":"response","isRequired":true,"strategy":"transform","extraRequests":[{"key":"rolesResp","method":"authorization.user-role.view","params":{"userId":"<%= result.user.id %>"}}],"convertParams":{"type":"jwt","userId":"$result.user.id","params":"$task.params.payload.headers.user-info","jwtPayload.roles":"$rolesResp.roles","returnType":"<%= _.get(task, \"params.payload.headers.user-info.authType\", \"cookies\") %>"},"convertResult":{"payload.cookies":"$middleware.payload.cookies","tokens.access":"$middleware.access","tokens.refresh":"$middleware.refresh"}}},{"target":"users","targetMethod":"user.sign-out","sender":"authentication","senderMethod":"token.remove","type":"response","order":10,"description":"Remove JWT auth token after successful sign out.","params":{"type":"response","isRequired":true,"strategy":"transform","extraRequests":[{"key":"rolesResp","method":"authentication.cookies.remove","condition":"<%= _.get(task, \"params.payload.headers.authType\", \"cookies\") === \"cookies\" %>"}],"convertParams":{"query.where.id":"$task.params.payload.authentication.tokenId"},"convertResult":{"payload.cookies":"$rolesResp.payload.cookies"}}},{"target":"users","targetMethod":"user.remove","sender":"authentication","senderMethod":"token.remove","type":"response","order":10,"description":"Remove JWT auth tokens after successful sign out.","params":{"type":"response","isRequired":true,"strategy":"transform","extraRequests":[{"key":"rolesResp","method":"authentication.cookies.remove","condition":"<%= _.get(task, \"params.payload.headers.authType\", \"cookies\") === \"cookies\" %>"}],"convertParams":{"query.where.userId":"$task.params.payload.authentication.userId"},"convertResult":{"payload.cookies":"$rolesResp.payload.cookies"}}}]'
33
MS_INIT_TASKS='[{"rule":"0 1 * * *","method":"users.confirm-code.remove","description":"Cleanup old confirmation codes","payload":{"params":{"query":{"where":{"expirationAt":{"<":"<%= Math.floor(Date.now() / 1000) %>"}}},"payload":{"authorization":{"filter":{"methodOptions":{"isAllowMultiple":true}}}}},"allowErrorCodes":[-33485],"responseTemplate":"<%= `deleted: ${deleted.length}` %>"}},{"rule":"0 1 * * *","method":"authentication.token.remove","description":"Cleanup old auth tokens","payload":{"params":{"query":{"where":{"expirationAt":{"<":"<%= Math.floor(Date.now() / 1000) %>"}}},"payload":{"authorization":{"filter":{"methodOptions":{"isAllowMultiple":true}}}}},"allowErrorCodes":[-33485],"responseTemplate":"<%= `deleted: ${deleted.length}` %>"}},{"rule":"* * * * *","method":"notification.job.task.process","description":"Process tasks","payload":{"responseTemplate":"<%= `Process tasks counts: total is ${total}, completed is ${completed} and failed is ${failed}` %>"}}]'
44
MS_IMPORT_PERMISSION=2

configs/config.local.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
"paymentMethods": ["bancontact", "card"],
120120
"apiKey": "your test key from stripe or other service",
121121
"config": {
122-
"apiVersion": "2022-11-15"
122+
"apiVersion": "2025-06-30.basil"
123123
},
124124
"payoutCoeff": 0.3,
125125
"webhookKeys": {

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,23 @@
6363
"admin": "allow"
6464
}
6565
},
66-
"product": "payment-stripe.Product",
6766
"metadata": {
6867
"in": {
6968
"admin": "allow"
7069
},
7170
"out": {
7271
"admin": "allow"
7372
}
74-
}
73+
},
74+
"customUnitAmount": {
75+
"in": {
76+
"admin": "allow"
77+
},
78+
"out": {
79+
"admin": "allow"
80+
}
81+
},
82+
"product": "payment-stripe.Product"
7583
},
7684
"createdAt": "2023-05-26T13:01:39.186Z"
7785
},
@@ -314,15 +322,7 @@
314322
}
315323
},
316324
"customer": "payment-stripe.Customer",
317-
"product": "payment-stripe.Product",
318-
"customAmount": {
319-
"in": {
320-
"admin": "allow"
321-
},
322-
"out": {
323-
"user": "allow"
324-
}
325-
}
325+
"product": "payment-stripe.Product"
326326
},
327327
"createdAt": "2023-05-26T13:01:39.186Z"
328328
},
@@ -1126,14 +1126,6 @@
11261126
"out": {
11271127
"admin": "allow"
11281128
}
1129-
},
1130-
"customAmount": {
1131-
"in": {
1132-
"admin": "allow"
1133-
},
1134-
"out": {
1135-
"admin": "allow"
1136-
}
11371129
}
11381130
},
11391131
"createdAt": "2023-05-26T13:01:39.186Z"
@@ -1845,6 +1837,14 @@
18451837
"out": {
18461838
"user": "allow"
18471839
}
1840+
},
1841+
"customUnitAmount": {
1842+
"in": {
1843+
"admin": "allow"
1844+
},
1845+
"out": {
1846+
"admin": "allow"
1847+
}
18481848
}
18491849
},
18501850
"createdAt": "2023-05-26T13:01:39.186Z"

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ interface IStripeMockParams {
2626
describe('services/payment-gateway/stripe', () => {
2727
const sandbox = sinon.createSandbox();
2828
const config: StripeTypes.StripeConfig = {
29-
apiVersion: '2022-11-15',
29+
apiVersion: '2025-06-30.basil',
3030
};
3131
const remoteConfigMock = {
3232
config: {
33-
apiVersion: '2022-11-15',
33+
apiVersion: '2025-06-30.basil',
3434
},
3535
paymentMethods: ['bancontact', 'card'],
3636
apiKey: 'fake-api-key',
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export default class AddCustomUnitAmountToPrice1753736526000 implements MigrationInterface {
4+
name = 'AddCustomUnitAmountToPrice1753736526000';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
// Add customUnitAmount column to price table
8+
await queryRunner.query(`
9+
ALTER TABLE "price"
10+
ADD COLUMN "customUnitAmount" jsonb DEFAULT NULL
11+
`);
12+
13+
// Remove customAmount column from transaction table
14+
await queryRunner.query(`
15+
ALTER TABLE "transaction"
16+
DROP
17+
COLUMN IF EXISTS "customAmount"
18+
`);
19+
}
20+
21+
public async down(queryRunner: QueryRunner): Promise<void> {
22+
// Remove customUnitAmount column from price table
23+
await queryRunner.query(`
24+
ALTER TABLE "price"
25+
DROP
26+
COLUMN "customUnitAmount"
27+
`);
28+
29+
// Add back customAmount column to transaction table
30+
await queryRunner.query(`
31+
ALTER TABLE "transaction"
32+
ADD COLUMN "customAmount" integer DEFAULT NULL
33+
`);
34+
}
35+
}

microservices/payment-stripe/package-lock.json

Lines changed: 16 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

microservices/payment-stripe/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"class-transformer": "^0.5.1",
4545
"class-validator": "^0.14.0",
4646
"class-validator-jsonschema": "^5.0.0",
47-
"stripe": "^11.16.0",
47+
"stripe": "^18.3.0",
4848
"typeorm": "0.2.41",
4949
"uuidv4": "^6.2.13",
5050
"lodash": "^4.17.21"

microservices/payment-stripe/src/constants/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const msNameDefault = 'payment-stripe';
88
const constants = {
99
...GetConstants({ msNameDefault, version, isBuild, packageName: name, withDb: true }),
1010
MS_API_KEY: process.env.MS_API_KEY ?? '',
11-
MS_CONFIG: JSON.parse(process.env.MS_CONFIG ?? '{"apiVersion": "2022-11-15"}'),
11+
MS_CONFIG: JSON.parse(process.env.MS_CONFIG ?? '{"apiVersion": "2025-06-30.basil"}'),
1212
MS_PAYMENT_METHODS: JSON.parse(process.env.MS_PAYMENT_METHODS ?? '["bancontact", "card"]'),
1313
MS_WEBHOOK_KEYS: JSON.parse(process.env.MS_WEBHOOK_KEYS ?? '{}'),
1414
MS_PAYOUT_COEFF: Number(process.env.MS_PAYOUT_COEFF) ?? 0.3,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ class Price {
4747
@Column({ type: 'jsonb', default: null })
4848
metadata: Record<string, any> | null;
4949

50+
@JSONSchema({
51+
description: 'Custom unit amount configuration for Pay What You Want pricing',
52+
})
53+
@Column({ type: 'jsonb', default: null })
54+
customUnitAmount: {
55+
preset?: number;
56+
minimum?: number;
57+
maximum?: number;
58+
} | null;
59+
5060
@IsTypeormDate()
5161
@CreateDateColumn()
5262
createdAt: Date;

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,6 @@ 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-
227218
@JSONSchema({
228219
description: `Sales tax or other, that should be paid to the government by tax collector. Tax included in the
229220
payment intent amount and storing as collected fees amount.`,

0 commit comments

Comments
 (0)