shutro-erp 0.3.0 → 0.3.2
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/README.md +58 -58
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +31 -1
- package/dist/account_portal/index.js +3 -2
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +31 -1
- package/dist/accounting/index.js +4 -0
- package/dist/assistant/routes.js +1 -1
- package/dist/automation/engine.js +1 -1
- package/dist/automation/web/messages.js +2 -2
- package/dist/cli/apps.d.ts +1 -1
- package/dist/cli/apps.js +14 -14
- package/dist/cli/bin.js +3 -0
- package/dist/cli/index.js +11 -11
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +60 -36
- package/dist/cli/launch.js +10 -7
- package/dist/cli/project.d.ts +7 -7
- package/dist/cli/project.js +25 -25
- package/dist/cli/prompt.js +2 -2
- package/dist/cli/templates.d.ts +3 -3
- package/dist/cli/templates.js +33 -28
- package/dist/cli/ui.d.ts +1 -8
- package/dist/cli/ui.js +36 -12
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +31 -1
- package/dist/integrations/inbound.d.ts +36 -0
- package/dist/integrations/inbound.js +171 -0
- package/dist/integrations/index.d.ts +18 -0
- package/dist/integrations/index.js +400 -0
- package/dist/integrations/models.d.ts +6 -0
- package/dist/integrations/models.js +119 -0
- package/dist/integrations/web/DeliveryTools.d.ts +4 -0
- package/dist/integrations/web/DeliveryTools.js +15 -0
- package/dist/integrations/web/EndpointTools.d.ts +7 -0
- package/dist/integrations/web/EndpointTools.js +25 -0
- package/dist/integrations/web/InboundTools.d.ts +4 -0
- package/dist/integrations/web/InboundTools.js +15 -0
- package/dist/integrations/web/WebhookTools.d.ts +7 -0
- package/dist/integrations/web/WebhookTools.js +25 -0
- package/dist/integrations/web/index.d.ts +16 -0
- package/dist/integrations/web/index.js +27 -0
- package/dist/integrations/web/messages.d.ts +2 -0
- package/dist/integrations/web/messages.js +53 -0
- package/dist/integrations/webhooks.d.ts +63 -0
- package/dist/integrations/webhooks.js +217 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +31 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +31 -1
- package/dist/l10n_in/einvoice.js +1 -1
- package/dist/l10n_in/web/messages.js +2 -2
- package/dist/mail/index.js +2 -2
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/loader.js +2 -2
- package/dist/modules-sdk/manifest.d.ts +48 -0
- package/dist/notify/messages.js +3 -3
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +1 -1
- package/dist/payment/.kosh/bindings.d.ts +22 -0
- package/dist/payment/.kosh/bindings.js +15 -0
- package/dist/payment/.kosh/schema.d.ts +1553 -0
- package/dist/payment/index.d.ts +34 -0
- package/dist/payment/index.js +375 -0
- package/dist/payment/module.d.ts +6853 -0
- package/dist/payment/module.js +74 -0
- package/dist/payment/providers.d.ts +64 -0
- package/dist/payment/providers.js +77 -0
- package/dist/payment/settle.d.ts +11 -0
- package/dist/payment/settle.js +59 -0
- package/dist/payment/sslcommerz.d.ts +12 -0
- package/dist/payment/sslcommerz.js +185 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +31 -1
- package/dist/portal/routes-documents.js +5 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +31 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +31 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +31 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +31 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +31 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +31 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +31 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +31 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +31 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +31 -1
- package/dist/sales/index.js +2 -0
- package/dist/server/api/client.js +1 -1
- package/dist/server/api/openapi.js +17 -1
- package/dist/server/api/rest.d.ts +6 -1
- package/dist/server/api/rest.js +11 -0
- package/dist/server/apps.js +5 -5
- package/dist/server/auth/api-keys.d.ts +13 -0
- package/dist/server/auth/api-keys.js +65 -0
- package/dist/server/auth/base.js +24 -0
- package/dist/server/auth/crypto.js +1 -1
- package/dist/server/auth/scopes.d.ts +12 -0
- package/dist/server/auth/scopes.js +51 -0
- package/dist/server/auth/service.d.ts +57 -1
- package/dist/server/auth/service.js +229 -5
- package/dist/server/demo.d.ts +1 -1
- package/dist/server/demo.js +1 -1
- package/dist/server/env-names.d.ts +2 -0
- package/dist/server/env-names.js +16 -0
- package/dist/server/host.js +5 -1
- package/dist/server/identity.d.ts +7 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/registry.js +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.js +45 -5
- package/dist/settings/routes.js +1 -1
- package/dist/settings/web/ApiKeysPage.d.ts +8 -0
- package/dist/settings/web/ApiKeysPage.js +70 -0
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +4 -0
- package/dist/settings/web/messages.js +62 -0
- package/dist/storage/routes.js +1 -1
- package/dist/studio/build.js +1 -1
- package/dist/studio/generate.js +4 -4
- package/dist/studio/module.js +5 -5
- package/dist/studio/web/messages.js +2 -2
- package/dist/ui/app.js +26 -3
- package/dist/ui/i18n/messages.js +2 -2
- package/dist/ui/pwa/worker.js +2 -2
- package/dist/ui/registry.d.ts +15 -0
- package/dist/ui/registry.js +20 -0
- package/package.json +15 -3
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data layer of online payments (RFC 0017 §5.5): a company's payment providers (the adapter, its mode, the
|
|
3
|
+
* journal the money arrives in, the inbound webhook its callbacks come through) and the transactions customers
|
|
4
|
+
* start from the portal (the invoice, the amount, the provider's reference, the state, the `account.payment` that
|
|
5
|
+
* settled it). Credentials are never in records: an adapter reads them from the environment; the callbacks' secret
|
|
6
|
+
* is the endpoint's.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
|
+
import { accountPortalShutro } from '../account_portal/index.js';
|
|
10
|
+
import { integrationsShutro } from '../integrations/index.js';
|
|
11
|
+
import { defineModel, defineModule, f } from './.kosh/bindings.js';
|
|
12
|
+
const ALL = ['read', 'write', 'create', 'unlink', 'export', 'import'];
|
|
13
|
+
/** A transaction's reference: what the provider is told and gives back. */
|
|
14
|
+
export const newReference = () => `PAY-${randomBytes(9).toString('base64url')}`;
|
|
15
|
+
/** The adapters Kosh ships; a module adding one extends this selection. */
|
|
16
|
+
export const PROVIDERS = {
|
|
17
|
+
fake: 'Test provider (no money moves)',
|
|
18
|
+
sslcommerz: 'SSLCommerz',
|
|
19
|
+
};
|
|
20
|
+
export const PaymentProvider = defineModel('payment.provider', {
|
|
21
|
+
label: 'Payment provider',
|
|
22
|
+
recName: 'name',
|
|
23
|
+
companyScoped: { required: true },
|
|
24
|
+
order: ['sequence asc', 'id asc'],
|
|
25
|
+
tracking: ['state', 'journal'],
|
|
26
|
+
access: { 'account.user': ['read'], 'account.manager': ALL },
|
|
27
|
+
fields: {
|
|
28
|
+
name: f.char({ required: true }),
|
|
29
|
+
code: f.selection(PROVIDERS, { required: true, default: 'fake', label: 'Provider' }),
|
|
30
|
+
state: f.selection({ disabled: 'Disabled', test: 'Test', live: 'Live' }, { required: true, default: 'test', label: 'Mode' }),
|
|
31
|
+
/** Where the money arrives: the payments are registered in this bank (or cash) journal. */
|
|
32
|
+
journal: f.many2one('account.journal', { required: true, checkCompany: true }),
|
|
33
|
+
/** The inbound webhook the provider's callbacks come through (made with the provider). */
|
|
34
|
+
endpoint: f.many2one('webhook.endpoint', { label: 'Callback endpoint' }),
|
|
35
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
export const PaymentTransaction = defineModel('payment.transaction', {
|
|
39
|
+
label: 'Payment transaction',
|
|
40
|
+
recName: 'reference',
|
|
41
|
+
companyScoped: { required: true },
|
|
42
|
+
order: ['id desc'],
|
|
43
|
+
tracking: ['state'],
|
|
44
|
+
access: {
|
|
45
|
+
'account.invoicing': ['read'],
|
|
46
|
+
'account.user': ['read', 'export'],
|
|
47
|
+
'account.manager': ['read', 'export'],
|
|
48
|
+
},
|
|
49
|
+
fields: {
|
|
50
|
+
reference: f.char({ required: true, index: 'unique', default: newReference }),
|
|
51
|
+
provider: f.many2one('payment.provider', { required: true, checkCompany: true }),
|
|
52
|
+
move: f.many2one('account.move', { required: true, label: 'Invoice', checkCompany: true }),
|
|
53
|
+
partner: f.many2one('res.partner', { label: 'Customer' }),
|
|
54
|
+
currency: f.many2one('res.currency', { required: true }),
|
|
55
|
+
amount: f.monetary({ currency: 'currency', required: true }),
|
|
56
|
+
state: f.selection({
|
|
57
|
+
pending: 'Pending',
|
|
58
|
+
done: 'Paid',
|
|
59
|
+
failed: 'Failed',
|
|
60
|
+
cancelled: 'Cancelled',
|
|
61
|
+
}, { required: true, default: 'pending' }),
|
|
62
|
+
providerReference: f.char({ label: 'Provider reference' }),
|
|
63
|
+
/** The payment registered when the provider confirmed. */
|
|
64
|
+
payment: f.many2one('account.payment', { label: 'Payment' }),
|
|
65
|
+
error: f.text(),
|
|
66
|
+
doneAt: f.datetime({ label: 'Paid at' }),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
export const paymentShutro = defineModule({
|
|
70
|
+
name: 'payment',
|
|
71
|
+
version: '0.1.0',
|
|
72
|
+
depends: [accountPortalShutro, integrationsShutro],
|
|
73
|
+
models: [PaymentProvider, PaymentTransaction],
|
|
74
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { InboundCall } from '../modules-sdk/index.js';
|
|
2
|
+
export type PaymentStatus = 'pending' | 'done' | 'failed' | 'cancelled';
|
|
3
|
+
export interface PaymentRequest {
|
|
4
|
+
/** The transaction's reference: what the provider gives back. */
|
|
5
|
+
readonly reference: string;
|
|
6
|
+
/** Decimal string in the currency's unit (`'1250.00'`). */
|
|
7
|
+
readonly amount: string;
|
|
8
|
+
/** ISO 4217 (`BDT`). */
|
|
9
|
+
readonly currency: string;
|
|
10
|
+
/** What is paid (`Invoice INV/2026/0042`). */
|
|
11
|
+
readonly description: string;
|
|
12
|
+
readonly customer: {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly email: string | null;
|
|
15
|
+
readonly phone: string | null;
|
|
16
|
+
readonly street: string | null;
|
|
17
|
+
readonly city: string | null;
|
|
18
|
+
readonly zip: string | null;
|
|
19
|
+
/** ISO 3166 alpha-2. */
|
|
20
|
+
readonly country: string | null;
|
|
21
|
+
};
|
|
22
|
+
/** Where the provider sends the customer back (GET or POST; the server forwards them to the portal). */
|
|
23
|
+
readonly returnUrl: string;
|
|
24
|
+
/** The inbound webhook the provider calls (absolute URL). */
|
|
25
|
+
readonly callbackUrl: string;
|
|
26
|
+
readonly mode: 'test' | 'live';
|
|
27
|
+
}
|
|
28
|
+
export interface PaymentResult {
|
|
29
|
+
readonly reference: string;
|
|
30
|
+
readonly status: PaymentStatus;
|
|
31
|
+
readonly providerReference: string | null;
|
|
32
|
+
/** What the provider says was paid; checked against the transaction. */
|
|
33
|
+
readonly amount?: string;
|
|
34
|
+
readonly currency?: string;
|
|
35
|
+
readonly error?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PaymentAdapter {
|
|
38
|
+
readonly code: string;
|
|
39
|
+
/** Whether its credentials are set (the portal offers only providers whose adapter can work). */
|
|
40
|
+
configured(mode: 'test' | 'live'): boolean;
|
|
41
|
+
/** Starts a payment; returns the page to send the customer to. */
|
|
42
|
+
createPayment(request: PaymentRequest): Promise<{
|
|
43
|
+
readonly redirectUrl: string;
|
|
44
|
+
}>;
|
|
45
|
+
/** A callback's event id, or `null` when it is not genuine (checked with the endpoint's secret). */
|
|
46
|
+
verifyCallback(call: InboundCall, secret: string): string | null | Promise<string | null>;
|
|
47
|
+
/** What a verified callback says. */
|
|
48
|
+
readCallback(call: InboundCall): PaymentResult | null;
|
|
49
|
+
/** The provider's word on a payment (null: unknown to it). */
|
|
50
|
+
status(reference: string, mode: 'test' | 'live'): Promise<PaymentResult | null>;
|
|
51
|
+
}
|
|
52
|
+
/** The fake adapter's header: HMAC-SHA256 (hex) of the raw body with the endpoint's secret. */
|
|
53
|
+
export declare const FAKE_SIGNATURE = "x-fake-signature";
|
|
54
|
+
export declare const fakeAdapter: PaymentAdapter & {
|
|
55
|
+
/** The payment the fake checkout shows. */
|
|
56
|
+
pending(reference: string): PaymentRequest | null;
|
|
57
|
+
/** The customer paid or declined on the fake checkout: the result and the signed callback body. */
|
|
58
|
+
complete(reference: string, paid: boolean, secret: string): {
|
|
59
|
+
body: string;
|
|
60
|
+
signature: string;
|
|
61
|
+
} | null;
|
|
62
|
+
};
|
|
63
|
+
/** The adapters by code. */
|
|
64
|
+
export declare const ADAPTERS: ReadonlyMap<string, PaymentAdapter>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment provider adapters (RFC 0017 §5.5). An adapter starts a payment at the provider (a page to send the customer
|
|
3
|
+
* to), reads the provider's callbacks (verified with the callback endpoint's secret), and asks the provider for a
|
|
4
|
+
* payment's status (when the customer returns before the callback). Credentials come from the environment, never
|
|
5
|
+
* from records. The fake adapter moves no money: its checkout page (served by this module) lets the customer pay or
|
|
6
|
+
* decline, and it calls back like a real provider, signed.
|
|
7
|
+
*/
|
|
8
|
+
import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
9
|
+
/** The fake adapter's header: HMAC-SHA256 (hex) of the raw body with the endpoint's secret. */
|
|
10
|
+
export const FAKE_SIGNATURE = 'x-fake-signature';
|
|
11
|
+
/** What the fake checkout knows (per process: it is for tests and demonstrations). */
|
|
12
|
+
const fakeLedger = new Map();
|
|
13
|
+
export const fakeAdapter = {
|
|
14
|
+
code: 'fake',
|
|
15
|
+
configured: (mode) => mode === 'test',
|
|
16
|
+
async createPayment(request) {
|
|
17
|
+
fakeLedger.set(request.reference, {
|
|
18
|
+
reference: request.reference,
|
|
19
|
+
status: 'pending',
|
|
20
|
+
providerReference: null,
|
|
21
|
+
request,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
redirectUrl: `/api/v1/payment/fake/checkout/${encodeURIComponent(request.reference)}`,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
verifyCallback(call, secret) {
|
|
28
|
+
const given = Buffer.from(call.headers[FAKE_SIGNATURE] ?? '');
|
|
29
|
+
const expected = Buffer.from(createHmac('sha256', secret).update(call.body).digest('hex'));
|
|
30
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
31
|
+
return null;
|
|
32
|
+
const id = call.json?.id;
|
|
33
|
+
return typeof id === 'string' && id !== '' ? id : null;
|
|
34
|
+
},
|
|
35
|
+
readCallback(call) {
|
|
36
|
+
const event = call.json;
|
|
37
|
+
if (typeof event?.reference !== 'string')
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
reference: event.reference,
|
|
41
|
+
status: event.status ?? 'pending',
|
|
42
|
+
providerReference: event.providerReference ?? null,
|
|
43
|
+
...(event.amount === undefined ? {} : { amount: event.amount }),
|
|
44
|
+
...(event.currency === undefined ? {} : { currency: event.currency }),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
async status(reference) {
|
|
48
|
+
const known = fakeLedger.get(reference);
|
|
49
|
+
if (known === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
const { request: _request, ...result } = known;
|
|
52
|
+
return result;
|
|
53
|
+
},
|
|
54
|
+
pending(reference) {
|
|
55
|
+
const known = fakeLedger.get(reference);
|
|
56
|
+
return known?.status === 'pending' ? known.request : null;
|
|
57
|
+
},
|
|
58
|
+
complete(reference, paid, secret) {
|
|
59
|
+
const known = fakeLedger.get(reference);
|
|
60
|
+
if (known === undefined || known.status !== 'pending')
|
|
61
|
+
return null;
|
|
62
|
+
const result = {
|
|
63
|
+
reference,
|
|
64
|
+
status: paid ? 'done' : 'failed',
|
|
65
|
+
providerReference: `FAKE-${randomUUID().slice(0, 8).toUpperCase()}`,
|
|
66
|
+
amount: known.request.amount,
|
|
67
|
+
currency: known.request.currency,
|
|
68
|
+
};
|
|
69
|
+
fakeLedger.set(reference, { ...result, request: known.request });
|
|
70
|
+
const body = JSON.stringify({ id: `evt_${randomUUID()}`, ...result });
|
|
71
|
+
return { body, signature: createHmac('sha256', secret).update(body).digest('hex') };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/** The adapters by code. */
|
|
75
|
+
export const ADAPTERS = new Map([
|
|
76
|
+
[fakeAdapter.code, fakeAdapter],
|
|
77
|
+
]);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Env } from './.kosh/bindings.js';
|
|
2
|
+
import type { PaymentResult } from './providers.js';
|
|
3
|
+
export interface Settled {
|
|
4
|
+
readonly reference: string;
|
|
5
|
+
readonly state: string;
|
|
6
|
+
/** False when it was settled before (a repeated callback, a return after the callback). */
|
|
7
|
+
readonly changed: boolean;
|
|
8
|
+
readonly payment: number | null;
|
|
9
|
+
}
|
|
10
|
+
/** Applies the provider's result to transaction `result.reference`, as the system in the transaction's company. */
|
|
11
|
+
export declare function settle(env: Env, result: PaymentResult): Promise<Settled | null>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settling a transaction (RFC 0017 §5.5): the provider's word — by its callback or asked when the customer returns —
|
|
3
|
+
* moves a pending transaction to paid, failed or cancelled. Paid registers an `account.payment` for the invoice in
|
|
4
|
+
* the provider's journal (Accounting posts and reconciles it), **once**: a transaction-scoped advisory lock on the
|
|
5
|
+
* reference serialises a callback and a return arriving together, and a paid transaction is never settled again
|
|
6
|
+
* (a failed or cancelled one becomes paid if the provider confirms it later).
|
|
7
|
+
*/
|
|
8
|
+
import { registerPayment } from '../accounting/index.js';
|
|
9
|
+
import { orm } from '../server/orm.js';
|
|
10
|
+
/** Applies the provider's result to transaction `result.reference`, as the system in the transaction's company. */
|
|
11
|
+
export async function settle(env, result) {
|
|
12
|
+
await env.sql `SELECT pg_advisory_xact_lock(hashtext(${`payment:${result.reference}`}))`;
|
|
13
|
+
const found = await env
|
|
14
|
+
.model('payment.transaction')
|
|
15
|
+
.search({ where: { reference: result.reference }, limit: 1 });
|
|
16
|
+
if (found.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const [tx] = await found.load({ provider: true, currency: true, move: true });
|
|
19
|
+
if (tx === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
const same = { reference: tx.reference, changed: false };
|
|
22
|
+
// Paid is final. A failure or cancellation is not: the provider may still confirm the payment later (a retry,
|
|
23
|
+
// a failure reported early) — then it is paid. Nothing else changes a settled transaction.
|
|
24
|
+
if (tx.state === 'done' ||
|
|
25
|
+
result.status === 'pending' ||
|
|
26
|
+
(tx.state !== 'pending' && result.status !== 'done'))
|
|
27
|
+
return { ...same, state: tx.state, payment: tx.payment?.id ?? null };
|
|
28
|
+
if (result.status !== 'done') {
|
|
29
|
+
await found.write({
|
|
30
|
+
state: result.status,
|
|
31
|
+
providerReference: result.providerReference,
|
|
32
|
+
error: result.error ?? (result.status === 'failed' ? 'The provider refused the payment.' : null),
|
|
33
|
+
});
|
|
34
|
+
return { ...same, state: result.status, changed: true, payment: null };
|
|
35
|
+
}
|
|
36
|
+
// What the provider says was paid must be what was asked.
|
|
37
|
+
const amount = tx.amount.amount;
|
|
38
|
+
if ((result.amount !== undefined && !orm.toDecimal(result.amount).eq(amount)) ||
|
|
39
|
+
(result.currency !== undefined && result.currency !== tx.currency.name)) {
|
|
40
|
+
const error = `The provider reports ${result.amount ?? '?'} ${result.currency ?? ''}, not ${amount.toFixed()} ${tx.currency.name}.`;
|
|
41
|
+
await found.write({ state: 'failed', providerReference: result.providerReference, error });
|
|
42
|
+
return { ...same, state: 'failed', changed: true, payment: null };
|
|
43
|
+
}
|
|
44
|
+
const payment = await registerPayment(env, {
|
|
45
|
+
moves: [tx.move.id],
|
|
46
|
+
journal: tx.provider.journal.id,
|
|
47
|
+
amount: amount.toFixed(),
|
|
48
|
+
memo: `${tx.provider.name} ${result.providerReference ?? tx.reference}`,
|
|
49
|
+
});
|
|
50
|
+
await found.write({
|
|
51
|
+
state: 'done',
|
|
52
|
+
providerReference: result.providerReference,
|
|
53
|
+
payment: payment.id,
|
|
54
|
+
error: null,
|
|
55
|
+
doneAt: new Date(),
|
|
56
|
+
});
|
|
57
|
+
await env.flush(); // checks run in this system scope, not at commit (Shutro request #11)
|
|
58
|
+
return { ...same, state: 'done', changed: true, payment: payment.id };
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PaymentAdapter } from './providers.js';
|
|
2
|
+
export interface SslcommerzOptions {
|
|
3
|
+
readonly storeId?: string | undefined;
|
|
4
|
+
readonly storePassword?: string | undefined;
|
|
5
|
+
/** The sandbox (default) or the live system. */
|
|
6
|
+
readonly sandbox?: boolean;
|
|
7
|
+
/** The HTTP client (tests). */
|
|
8
|
+
readonly fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export declare function sslcommerzAdapter(options?: SslcommerzOptions): PaymentAdapter;
|
|
11
|
+
/** SSLCommerz with its credentials from the environment, read on first use (after the server applied its names). */
|
|
12
|
+
export declare function sslcommerzFromEnvironment(env?: NodeJS.ProcessEnv): PaymentAdapter;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const HOSTS = {
|
|
2
|
+
sandbox: 'https://sandbox.sslcommerz.com',
|
|
3
|
+
live: 'https://securepay.sslcommerz.com',
|
|
4
|
+
};
|
|
5
|
+
/** The IPN's fields: a form post (or JSON, which some integrations send). */
|
|
6
|
+
function fieldsOf(call) {
|
|
7
|
+
if (call.json !== null && typeof call.json === 'object' && !Array.isArray(call.json))
|
|
8
|
+
return Object.fromEntries(Object.entries(call.json).map(([k, v]) => [k, String(v ?? '')]));
|
|
9
|
+
return Object.fromEntries(new URLSearchParams(call.body));
|
|
10
|
+
}
|
|
11
|
+
/** What was paid, in the transaction's currency (`currency_type` / `currency_amount` when it was not BDT). */
|
|
12
|
+
const paidAmount = (f) => f['currency_amount'] || f['amount'] || '';
|
|
13
|
+
const paidCurrency = (f) => f['currency_type'] || f['currency'] || '';
|
|
14
|
+
const same = (a, b) => a !== '' && b !== '' && Number(a).toFixed(2) === Number(b).toFixed(2);
|
|
15
|
+
const IPN_STATUS = {
|
|
16
|
+
VALID: 'done',
|
|
17
|
+
VALIDATED: 'done',
|
|
18
|
+
FAILED: 'failed',
|
|
19
|
+
CANCELLED: 'cancelled',
|
|
20
|
+
EXPIRED: 'cancelled',
|
|
21
|
+
};
|
|
22
|
+
const countryName = (code) => {
|
|
23
|
+
if (code === null || code === '')
|
|
24
|
+
return 'Bangladesh';
|
|
25
|
+
try {
|
|
26
|
+
return new Intl.DisplayNames(['en'], { type: 'region' }).of(code.toUpperCase()) ?? code;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return code;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export function sslcommerzAdapter(options = {}) {
|
|
33
|
+
const storeId = options.storeId ?? '';
|
|
34
|
+
const storePassword = options.storePassword ?? '';
|
|
35
|
+
const sandbox = options.sandbox ?? true;
|
|
36
|
+
const host = sandbox ? HOSTS.sandbox : HOSTS.live;
|
|
37
|
+
const http = options.fetch ?? fetch;
|
|
38
|
+
const ready = storeId !== '' && storePassword !== '';
|
|
39
|
+
const credentials = () => {
|
|
40
|
+
if (!ready)
|
|
41
|
+
throw new Error('SSLCommerz is not configured: set SHUTRO_ERP_SSLCOMMERZ_STORE_ID and SHUTRO_ERP_SSLCOMMERZ_STORE_PASSWORD.');
|
|
42
|
+
return { store_id: storeId, store_passwd: storePassword };
|
|
43
|
+
};
|
|
44
|
+
const getJson = async (path, query) => {
|
|
45
|
+
const url = `${host}${path}?${new URLSearchParams({ ...query, ...credentials(), format: 'json' })}`;
|
|
46
|
+
const res = await http(url, { signal: AbortSignal.timeout(15_000) });
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
throw new Error(`SSLCommerz answered ${res.status}.`);
|
|
49
|
+
return (await res.json());
|
|
50
|
+
};
|
|
51
|
+
/** The attempts SSLCommerz knows for a transaction id. */
|
|
52
|
+
const attempts = async (reference) => {
|
|
53
|
+
const answer = await getJson('/validator/api/merchantTransIDvalidationAPI.php', {
|
|
54
|
+
tran_id: reference,
|
|
55
|
+
});
|
|
56
|
+
if (answer['APIConnect'] !== 'DONE')
|
|
57
|
+
return null;
|
|
58
|
+
const element = Array.isArray(answer['element'])
|
|
59
|
+
? answer['element']
|
|
60
|
+
: [];
|
|
61
|
+
return element.map((e) => Object.fromEntries(Object.entries(e).map(([k, v]) => [k, String(v ?? '')])));
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
code: 'sslcommerz',
|
|
65
|
+
configured: (mode) => ready && (mode === 'test') === sandbox,
|
|
66
|
+
async createPayment(request) {
|
|
67
|
+
const body = new URLSearchParams({
|
|
68
|
+
...credentials(),
|
|
69
|
+
total_amount: request.amount,
|
|
70
|
+
currency: request.currency,
|
|
71
|
+
tran_id: request.reference,
|
|
72
|
+
success_url: request.returnUrl,
|
|
73
|
+
fail_url: request.returnUrl,
|
|
74
|
+
cancel_url: request.returnUrl,
|
|
75
|
+
ipn_url: request.callbackUrl,
|
|
76
|
+
cus_name: request.customer.name || 'Customer',
|
|
77
|
+
cus_email: request.customer.email ?? '',
|
|
78
|
+
cus_add1: request.customer.street ?? 'N/A',
|
|
79
|
+
cus_city: request.customer.city ?? 'N/A',
|
|
80
|
+
cus_postcode: request.customer.zip ?? '0000',
|
|
81
|
+
cus_country: countryName(request.customer.country),
|
|
82
|
+
cus_phone: request.customer.phone ?? 'N/A',
|
|
83
|
+
shipping_method: 'NO',
|
|
84
|
+
product_name: request.description,
|
|
85
|
+
product_category: 'Invoice',
|
|
86
|
+
product_profile: 'general',
|
|
87
|
+
emi_option: '0',
|
|
88
|
+
value_a: request.reference,
|
|
89
|
+
});
|
|
90
|
+
const res = await http(`${host}/gwprocess/v4/api.php`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
93
|
+
body,
|
|
94
|
+
signal: AbortSignal.timeout(15_000),
|
|
95
|
+
});
|
|
96
|
+
const answer = (await res.json().catch(() => ({})));
|
|
97
|
+
const page = answer['GatewayPageURL'];
|
|
98
|
+
if (answer['status'] !== 'SUCCESS' || typeof page !== 'string' || page === '')
|
|
99
|
+
throw new Error(`SSLCommerz refused the payment: ${String(answer['failedreason'] ?? res.status)}.`);
|
|
100
|
+
return { redirectUrl: page };
|
|
101
|
+
},
|
|
102
|
+
async verifyCallback(call) {
|
|
103
|
+
const f = fieldsOf(call);
|
|
104
|
+
const reference = f['tran_id'] ?? '';
|
|
105
|
+
const status = IPN_STATUS[f['status'] ?? ''];
|
|
106
|
+
if (reference === '' || status === undefined || !ready)
|
|
107
|
+
return null;
|
|
108
|
+
if (status === 'done') {
|
|
109
|
+
// Paid: SSLCommerz confirms this val_id, for this transaction, amount and currency.
|
|
110
|
+
const valId = f['val_id'] ?? '';
|
|
111
|
+
if (valId === '')
|
|
112
|
+
return null;
|
|
113
|
+
const v = await getJson('/validator/api/validationserverAPI.php', { val_id: valId });
|
|
114
|
+
const fv = Object.fromEntries(Object.entries(v).map(([k, x]) => [k, String(x ?? '')]));
|
|
115
|
+
const confirmed = (fv['status'] === 'VALID' || fv['status'] === 'VALIDATED') &&
|
|
116
|
+
fv['tran_id'] === reference &&
|
|
117
|
+
same(paidAmount(fv), paidAmount(f)) &&
|
|
118
|
+
paidCurrency(fv) === paidCurrency(f);
|
|
119
|
+
return confirmed ? valId : null;
|
|
120
|
+
}
|
|
121
|
+
// Failed or cancelled: SSLCommerz knows the transaction and no attempt of it succeeded.
|
|
122
|
+
const known = await attempts(reference);
|
|
123
|
+
if (known === null || known.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
if (known.some((a) => a['status'] === 'VALID' || a['status'] === 'VALIDATED'))
|
|
126
|
+
return null;
|
|
127
|
+
return `${reference}:${f['status']}:${f['val_id'] || f['tran_date'] || ''}`;
|
|
128
|
+
},
|
|
129
|
+
readCallback(call) {
|
|
130
|
+
const f = fieldsOf(call);
|
|
131
|
+
const status = IPN_STATUS[f['status'] ?? ''];
|
|
132
|
+
if ((f['tran_id'] ?? '') === '' || status === undefined)
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
reference: f['tran_id'],
|
|
136
|
+
status,
|
|
137
|
+
providerReference: f['bank_tran_id'] || f['val_id'] || null,
|
|
138
|
+
...(status === 'done' ? { amount: paidAmount(f), currency: paidCurrency(f) } : {}),
|
|
139
|
+
...(f['error'] ? { error: f['error'] } : {}),
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
async status(reference) {
|
|
143
|
+
if (!ready)
|
|
144
|
+
return null;
|
|
145
|
+
const known = await attempts(reference);
|
|
146
|
+
if (known === null || known.length === 0)
|
|
147
|
+
return null;
|
|
148
|
+
const paid = known.find((a) => a['status'] === 'VALID' || a['status'] === 'VALIDATED');
|
|
149
|
+
if (paid !== undefined)
|
|
150
|
+
return {
|
|
151
|
+
reference,
|
|
152
|
+
status: 'done',
|
|
153
|
+
providerReference: paid['bank_tran_id'] || paid['val_id'] || null,
|
|
154
|
+
amount: paidAmount(paid),
|
|
155
|
+
currency: paidCurrency(paid),
|
|
156
|
+
};
|
|
157
|
+
if (known.some((a) => a['status'] === 'PENDING'))
|
|
158
|
+
return { reference, status: 'pending', providerReference: null };
|
|
159
|
+
const last = known.at(-1);
|
|
160
|
+
return {
|
|
161
|
+
reference,
|
|
162
|
+
status: 'failed',
|
|
163
|
+
providerReference: last?.['val_id'] || null,
|
|
164
|
+
...(last?.['error'] ? { error: last['error'] } : {}),
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** SSLCommerz with its credentials from the environment, read on first use (after the server applied its names). */
|
|
170
|
+
export function sslcommerzFromEnvironment(env = process.env) {
|
|
171
|
+
let made;
|
|
172
|
+
const real = () => (made ??= sslcommerzAdapter({
|
|
173
|
+
storeId: env['KOSH_SSLCOMMERZ_STORE_ID'],
|
|
174
|
+
storePassword: env['KOSH_SSLCOMMERZ_STORE_PASSWORD'],
|
|
175
|
+
sandbox: env['KOSH_SSLCOMMERZ_SANDBOX'] !== 'false',
|
|
176
|
+
}));
|
|
177
|
+
return {
|
|
178
|
+
code: 'sslcommerz',
|
|
179
|
+
configured: (mode) => real().configured(mode),
|
|
180
|
+
createPayment: (request) => real().createPayment(request),
|
|
181
|
+
verifyCallback: (call, secret) => real().verifyCallback(call, secret),
|
|
182
|
+
readCallback: (call) => real().readCallback(call),
|
|
183
|
+
status: (reference, mode) => real().status(reference, mode),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: bae647ca883975a1
|
|
2
2
|
// Bindings of module `portal`: import the definition API from here to get types scoped to what it can see.
|
|
3
3
|
export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
|
|
4
4
|
import { definitions } from '../../server/orm.js';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: bae647ca883975a1
|
|
2
2
|
// Schema view of module `portal` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
|
|
3
3
|
import type { F } from '../../server/orm.js';
|
|
4
4
|
|
|
5
5
|
/** Model name → model type (RFC 0002 §7.7). */
|
|
6
6
|
export interface SchemaView {
|
|
7
7
|
readonly 'audit.event': AuditEvent;
|
|
8
|
+
readonly 'auth.api_key': AuthApiKey;
|
|
8
9
|
readonly 'auth.credential': AuthCredential;
|
|
9
10
|
readonly 'auth.session': AuthSession;
|
|
10
11
|
readonly 'auth.user_grant': AuthUserGrant;
|
|
@@ -45,6 +46,34 @@ export interface AuditEvent {
|
|
|
45
46
|
export interface AuditEventMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'audit.event'> {
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** `auth.api_key` (owner: base). */
|
|
50
|
+
export interface AuthApiKey {
|
|
51
|
+
readonly name: 'auth.api_key';
|
|
52
|
+
readonly fields: {
|
|
53
|
+
readonly id: F.Id<'auth.api_key'>;
|
|
54
|
+
readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
55
|
+
readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
56
|
+
readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
57
|
+
readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
58
|
+
readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
|
|
59
|
+
readonly name: F.Char<{ readonly required: true }>;
|
|
60
|
+
readonly user: F.Many2one<'res.users', { readonly required: true }>;
|
|
61
|
+
readonly tokenHash: F.Char<{ readonly required: true }>;
|
|
62
|
+
readonly prefix: F.Char<{ readonly required: true }>;
|
|
63
|
+
readonly scopes: F.Text<{ readonly required: true }>;
|
|
64
|
+
readonly expiresAt: F.Datetime<{ readonly required: true }>;
|
|
65
|
+
readonly lastUsedAt: F.Datetime;
|
|
66
|
+
readonly lastUsedIp: F.Char;
|
|
67
|
+
readonly revoked: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
68
|
+
readonly issuedBy: F.Many2one<'res.users'>;
|
|
69
|
+
};
|
|
70
|
+
readonly methods: AuthApiKeyMethods;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Methods of `auth.api_key`: declared ones and transitions. */
|
|
74
|
+
export interface AuthApiKeyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.api_key'> {
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
/** `auth.credential` (owner: base). */
|
|
49
78
|
export interface AuthCredential {
|
|
50
79
|
readonly name: 'auth.credential';
|
|
@@ -336,6 +365,7 @@ export interface ResUsers {
|
|
|
336
365
|
readonly companies: F.Many2many<'res.company', { readonly required: true }>;
|
|
337
366
|
readonly lang: F.Char<{ readonly default: true }>;
|
|
338
367
|
readonly tz: F.Char<{ readonly default: true }>;
|
|
368
|
+
readonly isService: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
339
369
|
readonly portalPartner: F.Many2one<'res.partner'>; // portal
|
|
340
370
|
};
|
|
341
371
|
readonly methods: ResUsersMethods;
|
|
@@ -207,6 +207,11 @@ export function registerPortalDocumentRoutes(context) {
|
|
|
207
207
|
? {}
|
|
208
208
|
: { lineFields: meta(d.lineModel, d.spec.lines.fields), lines }),
|
|
209
209
|
actions: await available(su, d, id),
|
|
210
|
+
payable: d.spec.payment !== undefined &&
|
|
211
|
+
(await ref(su, d.model.name).search({
|
|
212
|
+
where: { and: [{ id }, d.spec.payment.when] },
|
|
213
|
+
limit: 1,
|
|
214
|
+
})).length > 0,
|
|
210
215
|
pdf: d.spec.pdf !== undefined,
|
|
211
216
|
thread: d.spec.thread === true,
|
|
212
217
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 86d7229d7b275957
|
|
2
2
|
// Bindings of module `product`: import the definition API from here to get types scoped to what it can see.
|
|
3
3
|
export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
|
|
4
4
|
import { definitions } from '../../server/orm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 86d7229d7b275957
|
|
2
2
|
// Schema view of module `product` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
|
|
3
3
|
import type { F } from '../../server/orm.js';
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ import type { F } from '../../server/orm.js';
|
|
|
6
6
|
export interface SchemaView {
|
|
7
7
|
readonly 'account.tax': AccountTax;
|
|
8
8
|
readonly 'audit.event': AuditEvent;
|
|
9
|
+
readonly 'auth.api_key': AuthApiKey;
|
|
9
10
|
readonly 'auth.credential': AuthCredential;
|
|
10
11
|
readonly 'auth.session': AuthSession;
|
|
11
12
|
readonly 'auth.user_grant': AuthUserGrant;
|
|
@@ -73,6 +74,34 @@ export interface AuditEvent {
|
|
|
73
74
|
export interface AuditEventMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'audit.event'> {
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/** `auth.api_key` (owner: base). */
|
|
78
|
+
export interface AuthApiKey {
|
|
79
|
+
readonly name: 'auth.api_key';
|
|
80
|
+
readonly fields: {
|
|
81
|
+
readonly id: F.Id<'auth.api_key'>;
|
|
82
|
+
readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
83
|
+
readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
84
|
+
readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
85
|
+
readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
86
|
+
readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
|
|
87
|
+
readonly name: F.Char<{ readonly required: true }>;
|
|
88
|
+
readonly user: F.Many2one<'res.users', { readonly required: true }>;
|
|
89
|
+
readonly tokenHash: F.Char<{ readonly required: true }>;
|
|
90
|
+
readonly prefix: F.Char<{ readonly required: true }>;
|
|
91
|
+
readonly scopes: F.Text<{ readonly required: true }>;
|
|
92
|
+
readonly expiresAt: F.Datetime<{ readonly required: true }>;
|
|
93
|
+
readonly lastUsedAt: F.Datetime;
|
|
94
|
+
readonly lastUsedIp: F.Char;
|
|
95
|
+
readonly revoked: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
96
|
+
readonly issuedBy: F.Many2one<'res.users'>;
|
|
97
|
+
};
|
|
98
|
+
readonly methods: AuthApiKeyMethods;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Methods of `auth.api_key`: declared ones and transitions. */
|
|
102
|
+
export interface AuthApiKeyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.api_key'> {
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
/** `auth.credential` (owner: base). */
|
|
77
106
|
export interface AuthCredential {
|
|
78
107
|
readonly name: 'auth.credential';
|
|
@@ -307,6 +336,7 @@ export interface ResUsers {
|
|
|
307
336
|
readonly companies: F.Many2many<'res.company', { readonly required: true }>;
|
|
308
337
|
readonly lang: F.Char<{ readonly default: true }>;
|
|
309
338
|
readonly tz: F.Char<{ readonly default: true }>;
|
|
339
|
+
readonly isService: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
310
340
|
};
|
|
311
341
|
readonly methods: ResUsersMethods;
|
|
312
342
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 8538c294f98318cd
|
|
2
2
|
// Bindings of module `project`: import the definition API from here to get types scoped to what it can see.
|
|
3
3
|
export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
|
|
4
4
|
import { definitions } from '../../server/orm.js';
|