shutro-erp 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +156 -1
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +156 -1
- package/dist/assistant/index.js +1 -1
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +156 -1
- package/dist/integrations/index.js +12 -0
- package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
- package/dist/integrations/web/OAuthAppsPage.js +79 -0
- package/dist/integrations/web/index.d.ts +1 -0
- package/dist/integrations/web/index.js +3 -0
- package/dist/integrations/web/messages.js +58 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +156 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +156 -4
- package/dist/l10n_in/module.d.ts +0 -62
- package/dist/l10n_in/module.js +2 -4
- package/dist/messaging/web/index.js +1 -1
- package/dist/notify/web/index.js +1 -1
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +157 -2
- package/dist/payment/bkash.d.ts +28 -0
- package/dist/payment/bkash.js +169 -0
- package/dist/payment/index.d.ts +4 -0
- package/dist/payment/index.js +80 -24
- package/dist/payment/module.d.ts +7 -4
- package/dist/payment/module.js +3 -0
- package/dist/payment/money.d.ts +7 -0
- package/dist/payment/money.js +37 -0
- package/dist/payment/providers.d.ts +13 -2
- package/dist/payment/razorpay.d.ts +12 -0
- package/dist/payment/razorpay.js +149 -0
- package/dist/payment/sslcommerz.js +1 -1
- package/dist/payment/stripe.d.ts +15 -0
- package/dist/payment/stripe.js +182 -0
- package/dist/permissions/admin.d.ts +3 -0
- package/dist/permissions/admin.js +108 -10
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/roles.d.ts +2 -0
- package/dist/permissions/roles.js +236 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +156 -1
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +156 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +156 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +156 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +156 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +156 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +156 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +156 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +156 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +156 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +156 -1
- package/dist/server/auth/base.js +98 -0
- package/dist/server/auth/oauth.d.ts +21 -0
- package/dist/server/auth/oauth.js +579 -0
- package/dist/server/auth/routes.d.ts +3 -0
- package/dist/server/auth/routes.js +29 -0
- package/dist/server/auth/service.d.ts +56 -0
- package/dist/server/auth/service.js +185 -3
- package/dist/server/identity.d.ts +4 -0
- package/dist/server/images.d.ts +12 -0
- package/dist/server/images.js +139 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +1 -0
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.js +20 -4
- package/dist/settings/company.d.ts +34 -0
- package/dist/settings/company.js +189 -0
- package/dist/settings/index.d.ts +10 -10
- package/dist/settings/index.js +159 -30
- package/dist/settings/people.d.ts +16 -0
- package/dist/settings/people.js +507 -0
- package/dist/settings/security.d.ts +13 -0
- package/dist/settings/security.js +158 -0
- package/dist/settings/web/AuditPage.d.ts +8 -0
- package/dist/settings/web/AuditPage.js +74 -0
- package/dist/settings/web/CompaniesPage.d.ts +8 -0
- package/dist/settings/web/CompaniesPage.js +52 -0
- package/dist/settings/web/CompanyPage.d.ts +9 -0
- package/dist/settings/web/CompanyPage.js +105 -0
- package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
- package/dist/settings/web/ConnectedAppsPage.js +33 -0
- package/dist/settings/web/OverviewPage.d.ts +9 -0
- package/dist/settings/web/OverviewPage.js +77 -0
- package/dist/settings/web/PeoplePage.d.ts +9 -0
- package/dist/settings/web/PeoplePage.js +154 -0
- package/dist/settings/web/ProfilePage.d.ts +14 -0
- package/dist/settings/web/ProfilePage.js +196 -0
- package/dist/settings/web/RolesEditor.d.ts +35 -0
- package/dist/settings/web/RolesEditor.js +49 -0
- package/dist/settings/web/RolesPage.d.ts +9 -0
- package/dist/settings/web/RolesPage.js +296 -0
- package/dist/settings/web/SecurityPage.d.ts +8 -0
- package/dist/settings/web/SecurityPage.js +81 -0
- package/dist/settings/web/index.d.ts +10 -0
- package/dist/settings/web/index.js +31 -1
- package/dist/settings/web/messages.js +594 -0
- package/dist/ui/api.d.ts +8 -0
- package/dist/ui/app.d.ts +5 -0
- package/dist/ui/app.js +143 -33
- package/dist/ui/components/AppIcon.js +11 -1
- package/dist/ui/components/Avatar.d.ts +21 -0
- package/dist/ui/components/Avatar.js +71 -0
- package/dist/ui/components/primitives.d.ts +9 -1
- package/dist/ui/components/primitives.js +12 -4
- package/dist/ui/context.d.ts +10 -1
- package/dist/ui/context.js +60 -2
- package/dist/ui/i18n/messages.d.ts +76 -0
- package/dist/ui/i18n/messages.js +152 -0
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.js +3 -1
- package/dist/ui/lib/accent.d.ts +13 -0
- package/dist/ui/lib/accent.js +36 -0
- package/dist/ui/registry.d.ts +7 -1
- package/dist/ui/registry.js +10 -1
- package/dist/ui/search/ViewTabs.d.ts +14 -0
- package/dist/ui/search/ViewTabs.js +42 -0
- package/dist/ui/shell/Activate.d.ts +6 -0
- package/dist/ui/shell/Activate.js +39 -0
- package/dist/ui/shell/CommandPalette.d.ts +7 -1
- package/dist/ui/shell/CommandPalette.js +16 -2
- package/dist/ui/shell/Consent.d.ts +5 -0
- package/dist/ui/shell/Consent.js +35 -0
- package/dist/ui/shell/Home.js +85 -16
- package/dist/ui/shell/Sidebar.d.ts +27 -0
- package/dist/ui/shell/Sidebar.js +77 -0
- package/dist/ui/shell/TwoFactorSetup.d.ts +5 -0
- package/dist/ui/shell/TwoFactorSetup.js +32 -0
- package/dist/ui/shell/shortcuts.d.ts +3 -0
- package/dist/ui/shell/shortcuts.js +13 -4
- package/dist/ui/styles.css +70 -28
- package/dist/ui/views/DateField.d.ts +10 -0
- package/dist/ui/views/DateField.js +129 -0
- package/dist/ui/views/FormView.js +161 -21
- package/dist/ui/views/ListView.d.ts +2 -0
- package/dist/ui/views/ListView.js +57 -25
- package/dist/ui/views/StatusChip.d.ts +10 -0
- package/dist/ui/views/StatusChip.js +22 -0
- package/dist/ui/views/widgets.d.ts +3 -0
- package/dist/ui/views/widgets.js +26 -8
- package/dist/views/types.d.ts +7 -0
- package/dist/views/validate.js +4 -0
- package/package.json +2 -2
package/dist/payment/index.js
CHANGED
|
@@ -22,14 +22,21 @@ import { portalDocuments } from '../portal/index.js';
|
|
|
22
22
|
import { HttpProblem } from '../server/index.js';
|
|
23
23
|
import { orm } from '../server/orm.js';
|
|
24
24
|
import { action, form, list } from '../views/index.js';
|
|
25
|
+
import { bkashFromEnvironment } from './bkash.js';
|
|
25
26
|
import { PaymentProvider, PaymentTransaction, paymentShutro } from './module.js';
|
|
26
27
|
import { ADAPTERS, FAKE_SIGNATURE, fakeAdapter } from './providers.js';
|
|
28
|
+
import { razorpayFromEnvironment } from './razorpay.js';
|
|
27
29
|
import { settle } from './settle.js';
|
|
28
30
|
import { sslcommerzFromEnvironment } from './sslcommerz.js';
|
|
31
|
+
import { stripeFromEnvironment } from './stripe.js';
|
|
32
|
+
export { bkashAdapter, bkashFromEnvironment } from './bkash.js';
|
|
29
33
|
export { newReference, PaymentProvider, PaymentTransaction, PROVIDERS, paymentShutro, } from './module.js';
|
|
34
|
+
export { decimalsOf, fromSubunits, toSubunits } from './money.js';
|
|
30
35
|
export { ADAPTERS, FAKE_SIGNATURE, fakeAdapter, } from './providers.js';
|
|
36
|
+
export { razorpayAdapter, razorpayFromEnvironment } from './razorpay.js';
|
|
31
37
|
export { settle } from './settle.js';
|
|
32
38
|
export { sslcommerzAdapter, sslcommerzFromEnvironment, } from './sslcommerz.js';
|
|
39
|
+
export { stripeAdapter, stripeFromEnvironment } from './stripe.js';
|
|
33
40
|
const typed = (env) => env;
|
|
34
41
|
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
35
42
|
/** A callback endpoint per adapter: verified by the adapter with the endpoint's secret, settled as the system. */
|
|
@@ -158,25 +165,34 @@ function registerPaymentRoutes(context, adapters, urls) {
|
|
|
158
165
|
await su.flush(); // company checks run in this superuser scope, not at commit (Shutro request #11)
|
|
159
166
|
const [tx] = await created.load({});
|
|
160
167
|
const reference = tx?.reference ?? '';
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
168
|
+
let started;
|
|
169
|
+
try {
|
|
170
|
+
started = await adapter.createPayment({
|
|
171
|
+
reference,
|
|
172
|
+
amount: due.toFixed(move.currency.decimalPlaces ?? 2),
|
|
173
|
+
currency: move.currency.name,
|
|
174
|
+
description: `${d.spec.label} ${move.name}`,
|
|
175
|
+
customer: {
|
|
176
|
+
name: move.partner?.name ?? '',
|
|
177
|
+
email: move.partner?.email ?? null,
|
|
178
|
+
phone: move.partner?.phone ?? move.partner?.mobile ?? null,
|
|
179
|
+
street: move.partner?.street ?? null,
|
|
180
|
+
city: move.partner?.city ?? null,
|
|
181
|
+
zip: move.partner?.zip ?? null,
|
|
182
|
+
country: move.partner?.country ?? null,
|
|
183
|
+
},
|
|
184
|
+
returnUrl: `${urls.api}/api/v1/payment/return/${encodeURIComponent(reference)}`,
|
|
185
|
+
callbackUrl: `${urls.api}/api/v1/hooks/${token}`,
|
|
186
|
+
mode: provider.state === 'live' ? 'live' : 'test',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
throw new HttpProblem(502, 'provider-error', 'The payment could not start', error instanceof Error ? error.message : String(error));
|
|
191
|
+
}
|
|
192
|
+
if (started.providerReference !== undefined)
|
|
193
|
+
await created.write({ providerReference: started.providerReference });
|
|
194
|
+
await su.flush();
|
|
195
|
+
return { reference, redirectUrl: started.redirectUrl };
|
|
180
196
|
});
|
|
181
197
|
return reply.status(201).send(started);
|
|
182
198
|
});
|
|
@@ -200,6 +216,7 @@ function registerPaymentRoutes(context, adapters, urls) {
|
|
|
200
216
|
currency: t.currency.name,
|
|
201
217
|
code: t.provider.code,
|
|
202
218
|
mode: t.provider.state === 'live' ? 'live' : 'test',
|
|
219
|
+
providerReference: t.providerReference,
|
|
203
220
|
error: t.error,
|
|
204
221
|
};
|
|
205
222
|
});
|
|
@@ -215,7 +232,10 @@ function registerPaymentRoutes(context, adapters, urls) {
|
|
|
215
232
|
let error = found.error;
|
|
216
233
|
// Back before the callback: the provider's word settles it now (once; the callback then finds it settled).
|
|
217
234
|
if (state === 'pending') {
|
|
218
|
-
const result = await adapters
|
|
235
|
+
const result = await adapters
|
|
236
|
+
.get(found.code)
|
|
237
|
+
?.status(reference, found.mode, found.providerReference)
|
|
238
|
+
.catch(() => null);
|
|
219
239
|
if (result !== null && result !== undefined && result.status !== 'pending') {
|
|
220
240
|
const settled = await context.shutro.bootstrap({ reason: `payment ${reference}: the customer returned`, company: found.company }, (env) => settle(typed(env), result));
|
|
221
241
|
state = settled?.state ?? state;
|
|
@@ -267,12 +287,41 @@ function registerPaymentRoutes(context, adapters, urls) {
|
|
|
267
287
|
config: { auth: false },
|
|
268
288
|
handler: async (request, reply) => {
|
|
269
289
|
const reference = String(request.params['reference'] ?? '');
|
|
270
|
-
const
|
|
290
|
+
const tx = await context.shutro.bootstrap({ reason: 'payment return' }, (raw) => raw.asSuperuser({ reason: 'payment return: which invoice' }, async (su) => {
|
|
271
291
|
const [t] = await (await typed(su.withFlags({ checkCompany: false }))
|
|
272
292
|
.model('payment.transaction')
|
|
273
|
-
.search({ where: { reference }, limit: 1 })).load({});
|
|
274
|
-
return t
|
|
293
|
+
.search({ where: { reference }, limit: 1 })).load({ provider: true });
|
|
294
|
+
return t === undefined
|
|
295
|
+
? null
|
|
296
|
+
: {
|
|
297
|
+
move: t.move.id,
|
|
298
|
+
company: t.company.id,
|
|
299
|
+
state: t.state,
|
|
300
|
+
code: t.provider.code,
|
|
301
|
+
mode: t.provider.state === 'live' ? 'live' : 'test',
|
|
302
|
+
providerReference: t.providerReference,
|
|
303
|
+
};
|
|
275
304
|
}));
|
|
305
|
+
// Providers that complete a payment on the merchant's request (bKash) do it now; the adapter asks the
|
|
306
|
+
// provider itself — what the browser brings is only a hint.
|
|
307
|
+
const adapter = tx === null ? undefined : adapters.get(tx.code);
|
|
308
|
+
if (tx !== null && tx.state !== 'done' && adapter?.returned !== undefined) {
|
|
309
|
+
const query = Object.fromEntries(Object.entries((request.query ?? {})).map(([k, v]) => [k, String(v)]));
|
|
310
|
+
try {
|
|
311
|
+
const result = await adapter.returned({
|
|
312
|
+
reference,
|
|
313
|
+
providerReference: tx.providerReference,
|
|
314
|
+
mode: tx.mode,
|
|
315
|
+
query,
|
|
316
|
+
});
|
|
317
|
+
if (result !== null)
|
|
318
|
+
await context.shutro.bootstrap({ reason: `payment ${reference}: the customer returned`, company: tx.company }, (env) => settle(typed(env), result));
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
request.log.warn({ err: error, reference }, 'payment return: the provider did not answer');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const move = tx?.move ?? null;
|
|
276
325
|
return reply.redirect(move === null
|
|
277
326
|
? `${urls.portal}/`
|
|
278
327
|
: `${urls.portal}/d/${encodeURIComponent(payable)}/${move}?payment=${encodeURIComponent(reference)}`, 303);
|
|
@@ -300,7 +349,14 @@ function registerPaymentRoutes(context, adapters, urls) {
|
|
|
300
349
|
}
|
|
301
350
|
/** Online payments with explicit options (tests, installations wiring their own adapters). */
|
|
302
351
|
export function paymentModule(options = {}) {
|
|
303
|
-
const adapters = options.adapters ??
|
|
352
|
+
const adapters = options.adapters ??
|
|
353
|
+
new Map([
|
|
354
|
+
...ADAPTERS,
|
|
355
|
+
['sslcommerz', sslcommerzFromEnvironment()],
|
|
356
|
+
['bkash', bkashFromEnvironment()],
|
|
357
|
+
['razorpay', razorpayFromEnvironment()],
|
|
358
|
+
['stripe', stripeFromEnvironment()],
|
|
359
|
+
]);
|
|
304
360
|
return defineModule({
|
|
305
361
|
shutro: paymentShutro,
|
|
306
362
|
kosh: '^0.1.0',
|
package/dist/payment/module.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export declare const newReference: () => string;
|
|
|
4
4
|
export declare const PROVIDERS: {
|
|
5
5
|
readonly fake: 'Test provider (no money moves)';
|
|
6
6
|
readonly sslcommerz: 'SSLCommerz';
|
|
7
|
+
readonly bkash: 'bKash';
|
|
8
|
+
readonly razorpay: 'Razorpay';
|
|
9
|
+
readonly stripe: 'Stripe';
|
|
7
10
|
};
|
|
8
11
|
export declare const PaymentProvider: import("shutro").Model<"payment.provider", {
|
|
9
12
|
readonly id: import("shutro").IdField<"payment.provider">;
|
|
@@ -58,7 +61,7 @@ export declare const PaymentProvider: import("shutro").Model<"payment.provider",
|
|
|
58
61
|
readonly hasDefault: false;
|
|
59
62
|
readonly readonly: false;
|
|
60
63
|
}, string>;
|
|
61
|
-
readonly code: import("shutro").SelectionField<"fake" | "sslcommerz", {
|
|
64
|
+
readonly code: import("shutro").SelectionField<"bkash" | "fake" | "razorpay" | "sslcommerz" | "stripe", {
|
|
62
65
|
readonly required: true;
|
|
63
66
|
readonly computed: false;
|
|
64
67
|
readonly inverse: false;
|
|
@@ -119,7 +122,7 @@ export declare const PaymentProvider: import("shutro").Model<"payment.provider",
|
|
|
119
122
|
readonly hasDefault: false;
|
|
120
123
|
readonly readonly: false;
|
|
121
124
|
}, string>;
|
|
122
|
-
readonly code: import("shutro").SelectionField<"fake" | "sslcommerz", {
|
|
125
|
+
readonly code: import("shutro").SelectionField<"bkash" | "fake" | "razorpay" | "sslcommerz" | "stripe", {
|
|
123
126
|
readonly required: true;
|
|
124
127
|
readonly computed: false;
|
|
125
128
|
readonly inverse: false;
|
|
@@ -491,7 +494,7 @@ export declare const paymentShutro: import("shutro").Module<readonly [import("sh
|
|
|
491
494
|
readonly hasDefault: false;
|
|
492
495
|
readonly readonly: false;
|
|
493
496
|
}, string>;
|
|
494
|
-
readonly code: import("shutro").SelectionField<"fake" | "sslcommerz", {
|
|
497
|
+
readonly code: import("shutro").SelectionField<"bkash" | "fake" | "razorpay" | "sslcommerz" | "stripe", {
|
|
495
498
|
readonly required: true;
|
|
496
499
|
readonly computed: false;
|
|
497
500
|
readonly inverse: false;
|
|
@@ -552,7 +555,7 @@ export declare const paymentShutro: import("shutro").Module<readonly [import("sh
|
|
|
552
555
|
readonly hasDefault: false;
|
|
553
556
|
readonly readonly: false;
|
|
554
557
|
}, string>;
|
|
555
|
-
readonly code: import("shutro").SelectionField<"fake" | "sslcommerz", {
|
|
558
|
+
readonly code: import("shutro").SelectionField<"bkash" | "fake" | "razorpay" | "sslcommerz" | "stripe", {
|
|
556
559
|
readonly required: true;
|
|
557
560
|
readonly computed: false;
|
|
558
561
|
readonly inverse: false;
|
package/dist/payment/module.js
CHANGED
|
@@ -16,6 +16,9 @@ export const newReference = () => `PAY-${randomBytes(9).toString('base64url')}`;
|
|
|
16
16
|
export const PROVIDERS = {
|
|
17
17
|
fake: 'Test provider (no money moves)',
|
|
18
18
|
sslcommerz: 'SSLCommerz',
|
|
19
|
+
bkash: 'bKash',
|
|
20
|
+
razorpay: 'Razorpay',
|
|
21
|
+
stripe: 'Stripe',
|
|
19
22
|
};
|
|
20
23
|
export const PaymentProvider = defineModel('payment.provider', {
|
|
21
24
|
label: 'Payment provider',
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Amounts in a currency's subunits, as payment providers take them (`'1500.00'` INR ↔ 150000). */
|
|
2
|
+
/** Decimal places of a currency's subunit. */
|
|
3
|
+
export declare function decimalsOf(currency: string): number;
|
|
4
|
+
/** `'1500.00'` INR → 150000. */
|
|
5
|
+
export declare function toSubunits(amount: string, currency: string): number;
|
|
6
|
+
/** 150000 INR → `'1500.00'`. */
|
|
7
|
+
export declare function fromSubunits(subunits: number, currency: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Amounts in a currency's subunits, as payment providers take them (`'1500.00'` INR ↔ 150000). */
|
|
2
|
+
/** Currencies without subunits, and with three decimals (Stripe's and Razorpay's lists). */
|
|
3
|
+
const ZERO = new Set([
|
|
4
|
+
'BIF',
|
|
5
|
+
'CLP',
|
|
6
|
+
'DJF',
|
|
7
|
+
'GNF',
|
|
8
|
+
'JPY',
|
|
9
|
+
'KMF',
|
|
10
|
+
'KRW',
|
|
11
|
+
'MGA',
|
|
12
|
+
'PYG',
|
|
13
|
+
'RWF',
|
|
14
|
+
'UGX',
|
|
15
|
+
'VND',
|
|
16
|
+
'VUV',
|
|
17
|
+
'XAF',
|
|
18
|
+
'XOF',
|
|
19
|
+
'XPF',
|
|
20
|
+
]);
|
|
21
|
+
const THREE = new Set(['BHD', 'JOD', 'KWD', 'OMR', 'TND']);
|
|
22
|
+
/** Decimal places of a currency's subunit. */
|
|
23
|
+
export function decimalsOf(currency) {
|
|
24
|
+
const code = currency.toUpperCase();
|
|
25
|
+
return ZERO.has(code) ? 0 : THREE.has(code) ? 3 : 2;
|
|
26
|
+
}
|
|
27
|
+
/** `'1500.00'` INR → 150000. */
|
|
28
|
+
export function toSubunits(amount, currency) {
|
|
29
|
+
const d = decimalsOf(currency);
|
|
30
|
+
const [whole = '0', fraction = ''] = amount.split('.');
|
|
31
|
+
return Number(`${whole}${fraction.padEnd(d, '0').slice(0, d)}`);
|
|
32
|
+
}
|
|
33
|
+
/** 150000 INR → `'1500.00'`. */
|
|
34
|
+
export function fromSubunits(subunits, currency) {
|
|
35
|
+
const d = decimalsOf(currency);
|
|
36
|
+
return (subunits / 10 ** d).toFixed(d);
|
|
37
|
+
}
|
|
@@ -38,16 +38,27 @@ export interface PaymentAdapter {
|
|
|
38
38
|
readonly code: string;
|
|
39
39
|
/** Whether its credentials are set (the portal offers only providers whose adapter can work). */
|
|
40
40
|
configured(mode: 'test' | 'live'): boolean;
|
|
41
|
-
/** Starts a payment; returns the page to send the customer to. */
|
|
41
|
+
/** Starts a payment; returns the page to send the customer to (and the provider's id of it, when it has one). */
|
|
42
42
|
createPayment(request: PaymentRequest): Promise<{
|
|
43
43
|
readonly redirectUrl: string;
|
|
44
|
+
readonly providerReference?: string;
|
|
44
45
|
}>;
|
|
45
46
|
/** A callback's event id, or `null` when it is not genuine (checked with the endpoint's secret). */
|
|
46
47
|
verifyCallback(call: InboundCall, secret: string): string | null | Promise<string | null>;
|
|
47
48
|
/** What a verified callback says. */
|
|
48
49
|
readCallback(call: InboundCall): PaymentResult | null;
|
|
49
50
|
/** The provider's word on a payment (null: unknown to it). */
|
|
50
|
-
status(reference: string, mode: 'test' | 'live'): Promise<PaymentResult | null>;
|
|
51
|
+
status(reference: string, mode: 'test' | 'live', providerReference: string | null): Promise<PaymentResult | null>;
|
|
52
|
+
/**
|
|
53
|
+
* The customer is back (`query`: what the provider put in the return URL — a hint, never proof). Providers that
|
|
54
|
+
* complete a payment only when the merchant asks (bKash's execute) do it here; the result is settled at once.
|
|
55
|
+
*/
|
|
56
|
+
returned?(input: {
|
|
57
|
+
readonly reference: string;
|
|
58
|
+
readonly providerReference: string | null;
|
|
59
|
+
readonly mode: 'test' | 'live';
|
|
60
|
+
readonly query: Readonly<Record<string, string>>;
|
|
61
|
+
}): Promise<PaymentResult | null>;
|
|
51
62
|
}
|
|
52
63
|
/** The fake adapter's header: HMAC-SHA256 (hex) of the raw body with the endpoint's secret. */
|
|
53
64
|
export declare const FAKE_SIGNATURE = "x-fake-signature";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PaymentAdapter } from './providers.js';
|
|
2
|
+
export interface RazorpayOptions {
|
|
3
|
+
readonly keyId?: string | undefined;
|
|
4
|
+
readonly keySecret?: string | undefined;
|
|
5
|
+
/** The API's base (default `https://api.razorpay.com/v1`). */
|
|
6
|
+
readonly baseUrl?: string | undefined;
|
|
7
|
+
/** The HTTP client (tests). */
|
|
8
|
+
readonly fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export declare function razorpayAdapter(options?: RazorpayOptions): PaymentAdapter;
|
|
11
|
+
/** Razorpay with its keys from the environment, read on first use. */
|
|
12
|
+
export declare function razorpayFromEnvironment(env?: NodeJS.ProcessEnv): PaymentAdapter;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Razorpay adapter (India; RFC 0017 §5.5), on Payment Links: a link per transaction (`reference_id` = its
|
|
3
|
+
* reference; amounts in subunits) whose page the customer pays on, back to the return URL. Razorpay's webhook
|
|
4
|
+
* (`payment_link.paid`) is set up in its dashboard with the provider's endpoint URL and **the endpoint's secret**:
|
|
5
|
+
* `X-Razorpay-Signature` (HMAC-SHA256, hex, of the raw body) is checked with it, `x-razorpay-event-id` makes deliveries
|
|
6
|
+
* idempotent. On the customer's return (and when asked) the adapter fetches the link from Razorpay by the id it
|
|
7
|
+
* kept — what the browser brings is not used.
|
|
8
|
+
*
|
|
9
|
+
* Credentials come from the environment, never from records: `SHUTRO_ERP_RAZORPAY_KEY_ID` and
|
|
10
|
+
* `SHUTRO_ERP_RAZORPAY_KEY_SECRET`. Test keys (`rzp_test_…`) serve providers in test mode, live keys live ones.
|
|
11
|
+
*/
|
|
12
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
13
|
+
import { fromSubunits, toSubunits } from './money.js';
|
|
14
|
+
const text = (v) => (v === undefined || v === null ? '' : String(v));
|
|
15
|
+
/** What a payment link says of its payment. */
|
|
16
|
+
function linkResult(reference, link) {
|
|
17
|
+
if (text(link['reference_id']) !== reference)
|
|
18
|
+
return null;
|
|
19
|
+
const status = text(link['status']);
|
|
20
|
+
const currency = text(link['currency']) || 'INR';
|
|
21
|
+
if (status === 'paid') {
|
|
22
|
+
const payments = Array.isArray(link['payments']) ? link['payments'] : [];
|
|
23
|
+
return {
|
|
24
|
+
reference,
|
|
25
|
+
status: 'done',
|
|
26
|
+
providerReference: text(payments.at(-1)?.['payment_id']) || text(link['id']) || null,
|
|
27
|
+
amount: fromSubunits(Number(link['amount_paid'] ?? 0), currency),
|
|
28
|
+
currency,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (status === 'expired' || status === 'cancelled')
|
|
32
|
+
return { reference, status: 'cancelled', providerReference: text(link['id']) || null };
|
|
33
|
+
return { reference, status: 'pending', providerReference: text(link['id']) || null };
|
|
34
|
+
}
|
|
35
|
+
export function razorpayAdapter(options = {}) {
|
|
36
|
+
const keyId = options.keyId ?? '';
|
|
37
|
+
const keySecret = options.keySecret ?? '';
|
|
38
|
+
const base = (options.baseUrl || 'https://api.razorpay.com/v1').replace(/\/$/, '');
|
|
39
|
+
const http = options.fetch ?? fetch;
|
|
40
|
+
const ready = keyId !== '' && keySecret !== '';
|
|
41
|
+
const call = async (method, path, body) => {
|
|
42
|
+
if (!ready)
|
|
43
|
+
throw new Error('Razorpay is not configured: set SHUTRO_ERP_RAZORPAY_KEY_ID and SHUTRO_ERP_RAZORPAY_KEY_SECRET.');
|
|
44
|
+
const res = await http(`${base}${path}`, {
|
|
45
|
+
method,
|
|
46
|
+
headers: {
|
|
47
|
+
authorization: `Basic ${Buffer.from(`${keyId}:${keySecret}`).toString('base64')}`,
|
|
48
|
+
'content-type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
51
|
+
signal: AbortSignal.timeout(30_000),
|
|
52
|
+
});
|
|
53
|
+
const answer = (await res.json().catch(() => ({})));
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const error = (answer['error'] ?? {});
|
|
56
|
+
throw new Error(`Razorpay answered ${res.status}: ${text(error['description']) || 'no detail'}.`);
|
|
57
|
+
}
|
|
58
|
+
return answer;
|
|
59
|
+
};
|
|
60
|
+
const fetchLink = (id) => call('GET', `/payment_links/${encodeURIComponent(id)}`);
|
|
61
|
+
return {
|
|
62
|
+
code: 'razorpay',
|
|
63
|
+
configured: (mode) => ready && keyId.startsWith(mode === 'test' ? 'rzp_test_' : 'rzp_live_'),
|
|
64
|
+
async createPayment(request) {
|
|
65
|
+
const phone = (request.customer.phone ?? '').replace(/[^\d+]/g, '');
|
|
66
|
+
const link = await call('POST', '/payment_links', {
|
|
67
|
+
amount: toSubunits(request.amount, request.currency),
|
|
68
|
+
currency: request.currency,
|
|
69
|
+
reference_id: request.reference,
|
|
70
|
+
description: request.description.slice(0, 2048),
|
|
71
|
+
customer: {
|
|
72
|
+
name: request.customer.name,
|
|
73
|
+
...(request.customer.email === null ? {} : { email: request.customer.email }),
|
|
74
|
+
...(phone.length >= 8 && phone.length <= 14 ? { contact: phone } : {}),
|
|
75
|
+
},
|
|
76
|
+
notify: { sms: false, email: false },
|
|
77
|
+
reminder_enable: false,
|
|
78
|
+
callback_url: request.returnUrl,
|
|
79
|
+
callback_method: 'get',
|
|
80
|
+
});
|
|
81
|
+
const id = text(link['id']);
|
|
82
|
+
const page = text(link['short_url']);
|
|
83
|
+
if (id === '' || page === '')
|
|
84
|
+
throw new Error('Razorpay made no payment link.');
|
|
85
|
+
return { redirectUrl: page, providerReference: id };
|
|
86
|
+
},
|
|
87
|
+
verifyCallback(call, secret) {
|
|
88
|
+
const given = Buffer.from(call.headers['x-razorpay-signature'] ?? '');
|
|
89
|
+
const expected = Buffer.from(createHmac('sha256', secret).update(call.body).digest('hex'));
|
|
90
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
91
|
+
return null;
|
|
92
|
+
const id = call.headers['x-razorpay-event-id'] ?? '';
|
|
93
|
+
return id !== '' ? id : null;
|
|
94
|
+
},
|
|
95
|
+
readCallback(call) {
|
|
96
|
+
const event = call.json;
|
|
97
|
+
const payload = (event?.['payload'] ?? {});
|
|
98
|
+
const link = (payload['payment_link']?.['entity'] ??
|
|
99
|
+
null);
|
|
100
|
+
if (link === null)
|
|
101
|
+
return null;
|
|
102
|
+
const reference = text(link['reference_id']);
|
|
103
|
+
const kind = text(event?.['event']);
|
|
104
|
+
if (kind === 'payment_link.paid') {
|
|
105
|
+
const payment = (payload['payment']?.['entity'] ?? {});
|
|
106
|
+
const currency = text(link['currency']) || 'INR';
|
|
107
|
+
return {
|
|
108
|
+
reference,
|
|
109
|
+
status: 'done',
|
|
110
|
+
providerReference: text(payment['id']) || text(link['id']) || null,
|
|
111
|
+
amount: fromSubunits(Number(link['amount_paid'] ?? payment['amount'] ?? 0), currency),
|
|
112
|
+
currency,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (kind === 'payment_link.expired' || kind === 'payment_link.cancelled')
|
|
116
|
+
return { reference, status: 'cancelled', providerReference: text(link['id']) || null };
|
|
117
|
+
return null;
|
|
118
|
+
},
|
|
119
|
+
async returned({ reference, providerReference }) {
|
|
120
|
+
if (providerReference === null || providerReference === '')
|
|
121
|
+
return null;
|
|
122
|
+
const result = linkResult(reference, await fetchLink(providerReference));
|
|
123
|
+
return result?.status === 'pending' ? null : result;
|
|
124
|
+
},
|
|
125
|
+
async status(reference, _mode, providerReference) {
|
|
126
|
+
if (!ready || providerReference === null || providerReference === '')
|
|
127
|
+
return null;
|
|
128
|
+
return linkResult(reference, await fetchLink(providerReference));
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Razorpay with its keys from the environment, read on first use. */
|
|
133
|
+
export function razorpayFromEnvironment(env = process.env) {
|
|
134
|
+
let made;
|
|
135
|
+
const real = () => (made ??= razorpayAdapter({
|
|
136
|
+
keyId: env['KOSH_RAZORPAY_KEY_ID'],
|
|
137
|
+
keySecret: env['KOSH_RAZORPAY_KEY_SECRET'],
|
|
138
|
+
baseUrl: env['KOSH_RAZORPAY_BASE_URL'],
|
|
139
|
+
}));
|
|
140
|
+
return {
|
|
141
|
+
code: 'razorpay',
|
|
142
|
+
configured: (mode) => real().configured(mode),
|
|
143
|
+
createPayment: (request) => real().createPayment(request),
|
|
144
|
+
verifyCallback: (call, secret) => real().verifyCallback(call, secret),
|
|
145
|
+
readCallback: (call) => real().readCallback(call),
|
|
146
|
+
returned: (input) => real().returned?.(input) ?? Promise.resolve(null),
|
|
147
|
+
status: (reference, mode, providerReference) => real().status(reference, mode, providerReference),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -180,6 +180,6 @@ export function sslcommerzFromEnvironment(env = process.env) {
|
|
|
180
180
|
createPayment: (request) => real().createPayment(request),
|
|
181
181
|
verifyCallback: (call, secret) => real().verifyCallback(call, secret),
|
|
182
182
|
readCallback: (call) => real().readCallback(call),
|
|
183
|
-
status: (reference, mode) => real().status(reference, mode),
|
|
183
|
+
status: (reference, mode, providerReference) => real().status(reference, mode, providerReference),
|
|
184
184
|
};
|
|
185
185
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PaymentAdapter } from './providers.js';
|
|
2
|
+
export interface StripeOptions {
|
|
3
|
+
readonly secretKey?: string | undefined;
|
|
4
|
+
/** The API's base (default `https://api.stripe.com/v1`). */
|
|
5
|
+
readonly baseUrl?: string | undefined;
|
|
6
|
+
/** How old a signed event may be, in seconds (default 300). */
|
|
7
|
+
readonly tolerance?: number;
|
|
8
|
+
/** The HTTP client (tests). */
|
|
9
|
+
readonly fetch?: typeof fetch;
|
|
10
|
+
/** The clock (tests). */
|
|
11
|
+
readonly now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
export declare function stripeAdapter(options?: StripeOptions): PaymentAdapter;
|
|
14
|
+
/** Stripe with its key from the environment, read on first use. */
|
|
15
|
+
export declare function stripeFromEnvironment(env?: NodeJS.ProcessEnv): PaymentAdapter;
|