@endora-commerce/mod-credit-limits 0.100.0
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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/admin/index.d.ts +31 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +32 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/CreditLimitsPage.d.ts +4 -0
- package/dist/admin/pages/CreditLimitsPage.d.ts.map +1 -0
- package/dist/admin/pages/CreditLimitsPage.js +144 -0
- package/dist/admin/pages/CreditLimitsPage.js.map +1 -0
- package/dist/backend/entities/credit-limit-reservation.entity.d.ts +35 -0
- package/dist/backend/entities/credit-limit-reservation.entity.d.ts.map +1 -0
- package/dist/backend/entities/credit-limit-reservation.entity.js +99 -0
- package/dist/backend/entities/credit-limit-reservation.entity.js.map +1 -0
- package/dist/backend/entities/credit-limit-return-topup.entity.d.ts +29 -0
- package/dist/backend/entities/credit-limit-return-topup.entity.d.ts.map +1 -0
- package/dist/backend/entities/credit-limit-return-topup.entity.js +71 -0
- package/dist/backend/entities/credit-limit-return-topup.entity.js.map +1 -0
- package/dist/backend/entities/credit-limit.entity.d.ts +21 -0
- package/dist/backend/entities/credit-limit.entity.d.ts.map +1 -0
- package/dist/backend/entities/credit-limit.entity.js +66 -0
- package/dist/backend/entities/credit-limit.entity.js.map +1 -0
- package/dist/backend/index.d.ts +80 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +112 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +59 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +114 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/credit-limit-read.d.ts +23 -0
- package/dist/backend/services/credit-limit-read.d.ts.map +1 -0
- package/dist/backend/services/credit-limit-read.js +35 -0
- package/dist/backend/services/credit-limit-read.js.map +1 -0
- package/dist/backend/services/credit-limit-service.d.ts +179 -0
- package/dist/backend/services/credit-limit-service.d.ts.map +1 -0
- package/dist/backend/services/credit-limit-service.js +487 -0
- package/dist/backend/services/credit-limit-service.js.map +1 -0
- package/dist/backend/services/credit-topup.d.ts +33 -0
- package/dist/backend/services/credit-topup.d.ts.map +1 -0
- package/dist/backend/services/credit-topup.js +46 -0
- package/dist/backend/services/credit-topup.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +138 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T063333_credit_limits_init.d.ts +15 -0
- package/dist/migrations/20260425T063333_credit_limits_init.d.ts.map +1 -0
- package/dist/migrations/20260425T063333_credit_limits_init.js +56 -0
- package/dist/migrations/20260425T063333_credit_limits_init.js.map +1 -0
- package/dist/migrations/20260817T201111_credit_limits_return_topups.d.ts +17 -0
- package/dist/migrations/20260817T201111_credit_limits_return_topups.d.ts.map +1 -0
- package/dist/migrations/20260817T201111_credit_limits_return_topups.js +36 -0
- package/dist/migrations/20260817T201111_credit_limits_return_topups.js.map +1 -0
- package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.d.ts +44 -0
- package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.d.ts.map +1 -0
- package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.js +81 -0
- package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.js.map +1 -0
- package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.d.ts +29 -0
- package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.d.ts.map +1 -0
- package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.js +45 -0
- package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.js.map +1 -0
- package/dist/migrations/index.d.ts +39 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +44 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +93 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/credit_limits.md +53 -0
- package/i18n/en.json +9 -0
- package/i18n/pl.json +9 -0
- package/package.json +92 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Entity, Index, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { OrgScoped } from '@endora-commerce/platform/tenancy';
|
|
13
|
+
/**
|
|
14
|
+
* Per-Organization credit limit grant. One row per organization (enforced by
|
|
15
|
+
* the migration's unique index). `grantedAmount = 0` is the soft-revoke shape;
|
|
16
|
+
* the row is preserved for audit history.
|
|
17
|
+
*
|
|
18
|
+
* `@OrgScoped` (feature 050): reads are auto-confined to the ambient tenant. The
|
|
19
|
+
* grant/adjust routes additionally gate on org membership because inserts are not
|
|
20
|
+
* reachable by the column filter.
|
|
21
|
+
*/
|
|
22
|
+
let CreditLimit = class CreditLimit {
|
|
23
|
+
[OptionalProps];
|
|
24
|
+
id = randomUUID();
|
|
25
|
+
organizationId;
|
|
26
|
+
grantedAmount;
|
|
27
|
+
currency;
|
|
28
|
+
grantedByAdminUserId;
|
|
29
|
+
createdAt = new Date();
|
|
30
|
+
updatedAt = new Date();
|
|
31
|
+
};
|
|
32
|
+
__decorate([
|
|
33
|
+
PrimaryKey({ type: 'uuid' }),
|
|
34
|
+
__metadata("design:type", String)
|
|
35
|
+
], CreditLimit.prototype, "id", void 0);
|
|
36
|
+
__decorate([
|
|
37
|
+
Property({ type: 'uuid' }),
|
|
38
|
+
Index(),
|
|
39
|
+
__metadata("design:type", String)
|
|
40
|
+
], CreditLimit.prototype, "organizationId", void 0);
|
|
41
|
+
__decorate([
|
|
42
|
+
Property({ type: 'decimal', precision: 14, scale: 2 }),
|
|
43
|
+
__metadata("design:type", String)
|
|
44
|
+
], CreditLimit.prototype, "grantedAmount", void 0);
|
|
45
|
+
__decorate([
|
|
46
|
+
Property({ type: 'string', length: 3 }),
|
|
47
|
+
__metadata("design:type", String)
|
|
48
|
+
], CreditLimit.prototype, "currency", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
Property({ type: 'uuid', nullable: true }),
|
|
51
|
+
__metadata("design:type", Object)
|
|
52
|
+
], CreditLimit.prototype, "grantedByAdminUserId", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
Property({ type: 'datetime', onCreate: () => new Date() }),
|
|
55
|
+
__metadata("design:type", Date)
|
|
56
|
+
], CreditLimit.prototype, "createdAt", void 0);
|
|
57
|
+
__decorate([
|
|
58
|
+
Property({ type: 'datetime', onUpdate: () => new Date() }),
|
|
59
|
+
__metadata("design:type", Date)
|
|
60
|
+
], CreditLimit.prototype, "updatedAt", void 0);
|
|
61
|
+
CreditLimit = __decorate([
|
|
62
|
+
OrgScoped(),
|
|
63
|
+
Entity({ tableName: 'credit_limits' })
|
|
64
|
+
], CreditLimit);
|
|
65
|
+
export { CreditLimit };
|
|
66
|
+
//# sourceMappingURL=credit-limit.entity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credit-limit.entity.js","sourceRoot":"","sources":["../../../src/backend/entities/credit-limit.entity.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAE9D;;;;;;;;GAQG;AAGI,IAAM,WAAW,GAAjB,MAAM,WAAW;IACtB,CAAC,aAAa,CAAC,CAA6D;IAG5E,EAAE,GAAW,UAAU,EAAE,CAAC;IAI1B,cAAc,CAAU;IAGxB,aAAa,CAAU;IAGvB,QAAQ,CAAU;IAGlB,oBAAoB,CAAiB;IAGrC,SAAS,GAAS,IAAI,IAAI,EAAE,CAAC;IAG7B,SAAS,GAAS,IAAI,IAAI,EAAE,CAAC;CAC9B,CAAA;AApBC;IADC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;uCACH;AAI1B;IAFC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,EAAE;;mDACgB;AAGxB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;;kDAChC;AAGvB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;;6CACtB;AAGlB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;yDACN;AAGrC;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;8BAChD,IAAI;8CAAc;AAG7B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;8BAChD,IAAI;8CAAc;AAvBlB,WAAW;IAFvB,SAAS,EAAE;IACX,MAAM,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;GAC1B,WAAW,CAwBvB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { FastifyRequest } from 'fastify';
|
|
3
|
+
import type { CommandBus } from '@endora-commerce/platform/commands';
|
|
4
|
+
import type { EventBus } from '@endora-commerce/platform/events';
|
|
5
|
+
import type { ModuleContext, RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
6
|
+
import type { OrganizationInheritancePort } from '@endora-commerce/contracts';
|
|
7
|
+
import { CreditLimitService } from './services/credit-limit-service.js';
|
|
8
|
+
import { CreditLimit } from './entities/credit-limit.entity.js';
|
|
9
|
+
import { CreditLimitReservation } from './entities/credit-limit-reservation.entity.js';
|
|
10
|
+
import { CreditLimitReturnTopup } from './entities/credit-limit-return-topup.entity.js';
|
|
11
|
+
/**
|
|
12
|
+
* `credit_limits` — an optional Command Bus on the one write that must be
|
|
13
|
+
* audited (feature 072, wave 2, T101).
|
|
14
|
+
*
|
|
15
|
+
* `CreditLimitService` took `commandBus` as an optional constructor argument,
|
|
16
|
+
* documented as "audits `adjust` co-transactionally when provided". Adjusting a
|
|
17
|
+
* customer's credit limit is precisely the class of write Constitution XIII
|
|
18
|
+
* exists for — an admin moving how much money an organization may spend before
|
|
19
|
+
* paying — and the optional form means the audited path is the one you get by
|
|
20
|
+
* remembering. Both roots passed it, so nothing was unaudited; the shape goes
|
|
21
|
+
* anyway, for the same reason as the twelve before it.
|
|
22
|
+
*
|
|
23
|
+
* `inheritance` was optional too, and its absence is quieter and stranger:
|
|
24
|
+
* "Absent ⇒ flat behavior", meaning a descendant organization with no own limit
|
|
25
|
+
* transacts against nothing instead of against its nearest ancestor's. Not an
|
|
26
|
+
* error, not a refusal — a different and more permissive credit policy, chosen
|
|
27
|
+
* by omission.
|
|
28
|
+
*
|
|
29
|
+
* `creditLimitService` is a **port**: `orders` reserves against it and the
|
|
30
|
+
* credit-topup payment provider draws on it, both across a module boundary, so
|
|
31
|
+
* an operator switching credit limits off should get the explicit 503 rather
|
|
32
|
+
* than a service that reports no limit — which on a checkout path means
|
|
33
|
+
* unlimited credit.
|
|
34
|
+
*/
|
|
35
|
+
export interface CreditLimitsCradle {
|
|
36
|
+
readonly emFactory: () => EntityManager;
|
|
37
|
+
readonly eventBus: EventBus;
|
|
38
|
+
readonly commandBus: CommandBus;
|
|
39
|
+
readonly requireAdmin: RequireAdminFactory;
|
|
40
|
+
readonly requireCustomer: (request: FastifyRequest) => Promise<void>;
|
|
41
|
+
/** How this composition resolves the calling customer; root-shaped. */
|
|
42
|
+
readonly customerContextResolver: (request: FastifyRequest) => {
|
|
43
|
+
customerAccountId: string;
|
|
44
|
+
organizationId: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Who the acting admin is, from the production actor (feature 080, T051).
|
|
48
|
+
* Root-supplied, like the other request resolvers; both roots answer it from
|
|
49
|
+
* `request.actor` and throw 401 for a non-admin.
|
|
50
|
+
*/
|
|
51
|
+
readonly adminContextResolver: (req: FastifyRequest) => {
|
|
52
|
+
adminUserId: string;
|
|
53
|
+
};
|
|
54
|
+
/** A port `organizations` provides — read it per call, never captured. */
|
|
55
|
+
readonly organizationInheritancePort: OrganizationInheritancePort;
|
|
56
|
+
readonly creditLimitService: CreditLimitService;
|
|
57
|
+
}
|
|
58
|
+
export declare function registerModule(ctx: ModuleContext): void;
|
|
59
|
+
/**
|
|
60
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
61
|
+
* array and **no named class export** (D-168).
|
|
62
|
+
*
|
|
63
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
64
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
65
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
66
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
67
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
68
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
69
|
+
*
|
|
70
|
+
* Three classes, and this module is where the absent named export and a foreign
|
|
71
|
+
* key had to be shown to coexist (D-169): `credit_limit_reservations` carries
|
|
72
|
+
* `credit_limit_reservations_order_fk` into `orders.id`, and a constraint is
|
|
73
|
+
* between two column names — it needs the **table**, never the class. Nothing
|
|
74
|
+
* another module can legitimately do with `CreditLimitReservation` is lost.
|
|
75
|
+
* D-32 forbids an ORM relation into it; what `orders` actually needs is the
|
|
76
|
+
* *method* placement calls, which leaves this package as `CreditLimitPort` on
|
|
77
|
+
* the type-only `./ports` subpath.
|
|
78
|
+
*/
|
|
79
|
+
export declare const entities: (typeof CreditLimit | typeof CreditLimitReservation | typeof CreditLimitReturnTopup)[];
|
|
80
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE3F,OAAO,KAAK,EAGV,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAA4B,MAAM,oCAAoC,CAAC;AAIlG,OAAO,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AAExF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,eAAe,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,uEAAuE;IACvE,QAAQ,CAAC,uBAAuB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK;QAC7D,iBAAiB,EAAE,MAAM,CAAC;QAC1B,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAChF,0EAA0E;IAC1E,QAAQ,CAAC,2BAA2B,EAAE,2BAA2B,CAAC;IAClE,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;CACjD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAyGvD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,QAAQ,wFAAgE,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { CreditLimitService } from './services/credit-limit-service.js';
|
|
3
|
+
import { CreditLimitReadService } from './services/credit-limit-read.js';
|
|
4
|
+
import { CreditTopupProvider } from './services/credit-topup.js';
|
|
5
|
+
import { registerCreditLimitsRoutes } from './routes.js';
|
|
6
|
+
import { CreditLimit } from './entities/credit-limit.entity.js';
|
|
7
|
+
import { CreditLimitReservation } from './entities/credit-limit-reservation.entity.js';
|
|
8
|
+
import { CreditLimitReturnTopup } from './entities/credit-limit-return-topup.entity.js';
|
|
9
|
+
export function registerModule(ctx) {
|
|
10
|
+
ctx.di.providePort('creditLimitService', ctx
|
|
11
|
+
.asFunction(({ emFactory, eventBus, commandBus }) => new CreditLimitService(emFactory, eventBus, commandBus,
|
|
12
|
+
// Resolved per call, not destructured. `organizationInheritancePort`
|
|
13
|
+
// is a port `organizations` provides, which makes it a transient gate
|
|
14
|
+
// on that module's effective state: capturing it in this singleton's
|
|
15
|
+
// factory is refused by awilix strict mode, and a captured gate would
|
|
16
|
+
// keep answering after an operator switched `organizations` off.
|
|
17
|
+
// Resolving at the point of use also means this registration does not
|
|
18
|
+
// care which module registered first — registration resolves nothing.
|
|
19
|
+
// `creditOwner` is the only method reached, and since Phase C the
|
|
20
|
+
// delegate is typed by `organizations`' published contract rather
|
|
21
|
+
// than cast onto its service class, so this module names nothing in
|
|
22
|
+
// that module's directory.
|
|
23
|
+
{
|
|
24
|
+
creditOwner: (orgId) => ctx.cradle().organizationInheritancePort.creditOwner(orgId),
|
|
25
|
+
}))
|
|
26
|
+
.singleton());
|
|
27
|
+
/**
|
|
28
|
+
* The membership read `organizations` needs to resolve an inherited limit
|
|
29
|
+
* (feature 077, D-87).
|
|
30
|
+
*
|
|
31
|
+
* `OrganizationInheritanceService.creditOwner` walks an ancestor chain and
|
|
32
|
+
* has to know which of those organisations hold a row here. It selected from
|
|
33
|
+
* this module's table to find out — a statement naming no import specifier,
|
|
34
|
+
* so the boundary compiled and nothing gated it, and the answer stayed the
|
|
35
|
+
* same after an operator switched credit limits off.
|
|
36
|
+
*
|
|
37
|
+
* A **read** port and deliberately not a method on `creditLimitService`: that
|
|
38
|
+
* service is the gated write seam whose `getForOrganization` asks
|
|
39
|
+
* `organizations` the very question this answers, so widening it would put
|
|
40
|
+
* both directions on one name. Separate names keep the two hops legible —
|
|
41
|
+
* this module asks who the owner is, `organizations` asks who holds a row —
|
|
42
|
+
* and neither recurses.
|
|
43
|
+
*
|
|
44
|
+
* `filters: { org: false }` is load-bearing rather than defensive: the holder
|
|
45
|
+
* is by definition an ancestor outside the caller's tenant scope, which is
|
|
46
|
+
* the whole point of the inheritance, so the org filter would answer the
|
|
47
|
+
* empty set for every descendant.
|
|
48
|
+
*/
|
|
49
|
+
ctx.di.providePort('creditLimitReadPort', ctx
|
|
50
|
+
.asFunction(({ emFactory }) => new CreditLimitReadService(emFactory))
|
|
51
|
+
.singleton());
|
|
52
|
+
/**
|
|
53
|
+
* How a return settlement credits an organization (feature 046 R7), as this
|
|
54
|
+
* module's port instead of a class both roots constructed (T143c).
|
|
55
|
+
*
|
|
56
|
+
* The provider is a thin rule over `CreditLimitService` and lives in this
|
|
57
|
+
* module already; what a root added was a second, ungated way to reach it, so
|
|
58
|
+
* a settlement kept topping up a grant with `credit_limits` switched off. The
|
|
59
|
+
* service is still handed in as an accessor, for the reason the routes below
|
|
60
|
+
* take it lazily: it is this module's own gated port, and resolving it while
|
|
61
|
+
* this registration is built asks the gate at composition time.
|
|
62
|
+
*/
|
|
63
|
+
ctx.di.providePort('creditTopupPort', ctx
|
|
64
|
+
.asFunction(() => {
|
|
65
|
+
const creditLimitService = lazyPort(ctx, 'creditLimitService');
|
|
66
|
+
return new CreditTopupProvider(() => creditLimitService);
|
|
67
|
+
})
|
|
68
|
+
.singleton());
|
|
69
|
+
ctx.routes(async (app) => {
|
|
70
|
+
const { requireCustomer, requireAdmin, customerContextResolver } = ctx.cradle();
|
|
71
|
+
await registerCreditLimitsRoutes(app, {
|
|
72
|
+
// Lazily, even though the module owns this port: `providePort` gates on
|
|
73
|
+
// the module's effective state and route *registration* happens whatever
|
|
74
|
+
// that state is — only requests are gated. Destructuring it here asked
|
|
75
|
+
// the gate while `buildServer` was wiring the app, so an operator who had
|
|
76
|
+
// switched `credit_limits` off stopped the backend from starting instead
|
|
77
|
+
// of stopping its routes. Inside a handler the gate is open by
|
|
78
|
+
// construction, so nothing else changes.
|
|
79
|
+
creditLimitService: lazyPort(ctx, 'creditLimitService'),
|
|
80
|
+
// `organizations`' port, lazily for the same reason and one more: it is
|
|
81
|
+
// another module's gate, so a captured reference would keep answering
|
|
82
|
+
// after an operator switched that module off.
|
|
83
|
+
organizationDetailsPort: lazyPort(ctx, 'organizationDetailsPort'),
|
|
84
|
+
requireCustomer,
|
|
85
|
+
requireAdmin,
|
|
86
|
+
resolveCustomerContext: customerContextResolver,
|
|
87
|
+
resolveAdminUserId: (request) => ctx.cradle().adminContextResolver(request).adminUserId,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
93
|
+
* array and **no named class export** (D-168).
|
|
94
|
+
*
|
|
95
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
96
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
97
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
98
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
99
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
100
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
101
|
+
*
|
|
102
|
+
* Three classes, and this module is where the absent named export and a foreign
|
|
103
|
+
* key had to be shown to coexist (D-169): `credit_limit_reservations` carries
|
|
104
|
+
* `credit_limit_reservations_order_fk` into `orders.id`, and a constraint is
|
|
105
|
+
* between two column names — it needs the **table**, never the class. Nothing
|
|
106
|
+
* another module can legitimately do with `CreditLimitReservation` is lost.
|
|
107
|
+
* D-32 forbids an ORM relation into it; what `orders` actually needs is the
|
|
108
|
+
* *method* placement calls, which leaves this package as `CreditLimitPort` on
|
|
109
|
+
* the type-only `./ports` subpath.
|
|
110
|
+
*/
|
|
111
|
+
export const entities = [CreditLimit, CreditLimitReservation, CreditLimitReturnTopup];
|
|
112
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAM5D,OAAO,EAAE,kBAAkB,EAA4B,MAAM,oCAAoC,CAAC;AAClG,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AAiDxF,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,oBAAoB,EACpB,GAAG;SACA,UAAU,CACT,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAsB,EAAE,EAAE,CAC1D,IAAI,kBAAkB,CACpB,SAAS,EACT,QAA0C,EAC1C,UAAU;IACV,qEAAqE;IACrE,sEAAsE;IACtE,qEAAqE;IACrE,sEAAsE;IACtE,iEAAiE;IACjE,sEAAsE;IACtE,sEAAsE;IACtE,kEAAkE;IAClE,kEAAkE;IAClE,oEAAoE;IACpE,2BAA2B;IAC3B;QACE,WAAW,EAAE,CAAC,KAAa,EAAE,EAAE,CAC7B,GAAG,CAAC,MAAM,EAAsB,CAAC,2BAA2B,CAAC,WAAW,CAAC,KAAK,CAAC;KAClF,CACF,CACJ;SACA,SAAS,EAAE,CACf,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,qBAAqB,EACrB,GAAG;SACA,UAAU,CACT,CAAC,EAAE,SAAS,EAAsB,EAAE,EAAE,CAAC,IAAI,sBAAsB,CAAC,SAAS,CAAC,CAC7E;SACA,SAAS,EAAE,CACf,CAAC;IAEF;;;;;;;;;;OAUG;IACH,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,iBAAiB,EACjB,GAAG;SACA,UAAU,CAAC,GAAG,EAAE;QACf,MAAM,kBAAkB,GAAG,QAAQ,CAAqB,GAAG,EAAE,oBAAoB,CAAC,CAAC;QACnF,OAAO,IAAI,mBAAmB,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,CAAC;IAC3D,CAAC,CAAC;SACD,SAAS,EAAE,CACf,CAAC;IAEF,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EAAE,eAAe,EAAE,YAAY,EAAE,uBAAuB,EAAE,GAC9D,GAAG,CAAC,MAAM,EAAsB,CAAC;QACnC,MAAM,0BAA0B,CAAC,GAAG,EAAE;YACpC,wEAAwE;YACxE,yEAAyE;YACzE,uEAAuE;YACvE,0EAA0E;YAC1E,yEAAyE;YACzE,+DAA+D;YAC/D,yCAAyC;YACzC,kBAAkB,EAAE,QAAQ,CAAqB,GAAG,EAAE,oBAAoB,CAAC;YAC3E,wEAAwE;YACxE,sEAAsE;YACtE,8CAA8C;YAC9C,uBAAuB,EAAE,QAAQ,CAA0B,GAAG,EAAE,yBAAyB,CAAC;YAC1F,eAAe;YACf,YAAY;YACZ,sBAAsB,EAAE,uBAAuB;YAC/C,kBAAkB,EAAE,CAAC,OAAO,EAAE,EAAE,CAC9B,GAAG,CAAC,MAAM,EAAsB,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,WAAW;SAC7E,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,WAAW,EAAE,sBAAsB,EAAE,sBAAsB,CAAC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import { type OrganizationDetailsPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { CreditLimitService } from './services/credit-limit-service.js';
|
|
4
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* `grantedByAdminUserId` is supplied by the host through `resolveAdminUserId`,
|
|
7
|
+
* not read off the request here.
|
|
8
|
+
*
|
|
9
|
+
* This module used to import `testAdminUserId` from the host's
|
|
10
|
+
* `test-actor-carrier.ts`, which `host-package.md` §1.4j classifies **A** —
|
|
11
|
+
* host-internal, deliberately absent from `@endora-commerce/platform/http`'s
|
|
12
|
+
* barrel, because it exists to narrow *this repository's* test-harness Fastify
|
|
13
|
+
* augmentation and an installed package has no relationship to it. Packaging
|
|
14
|
+
* this module is the first time that classification had to bite.
|
|
15
|
+
*
|
|
16
|
+
* The repair is not the carrier-declared-locally idiom §1.4n used for
|
|
17
|
+
* `productAudienceOf`, because that would have preserved a live defect: nothing
|
|
18
|
+
* under `src/` ever writes `request.testActor`, so a production grant recorded
|
|
19
|
+
* **no** `grantedByAdminUserId` at all — the audit trail for a financial grant,
|
|
20
|
+
* empty, with every harness test green because the harness mirrors its actor
|
|
21
|
+
* onto both fields. The host now injects the resolver, so the production path
|
|
22
|
+
* and the harness path read the same thing.
|
|
23
|
+
*/
|
|
24
|
+
export interface CreditLimitsDeps {
|
|
25
|
+
creditLimitService: CreditLimitService;
|
|
26
|
+
/**
|
|
27
|
+
* `organizationDetailsPort`, owned by `organizations` — the roster's one
|
|
28
|
+
* question about that table (feature 075, Phase C). It replaces an
|
|
29
|
+
* `em.find(Organization, …)`, which linked this module against another
|
|
30
|
+
* module's entity class at build time and kept answering with `organizations`
|
|
31
|
+
* switched off. The port fails the request closed instead.
|
|
32
|
+
*/
|
|
33
|
+
organizationDetailsPort: OrganizationDetailsPort;
|
|
34
|
+
requireCustomer: (req: FastifyRequest, reply: unknown) => Promise<void>;
|
|
35
|
+
requireAdmin: RequireAdminFactory;
|
|
36
|
+
resolveCustomerContext: (req: FastifyRequest) => {
|
|
37
|
+
customerAccountId: string;
|
|
38
|
+
organizationId: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The acting admin's id, resolved from the request the guard in front of
|
|
42
|
+
* these routes has already accepted.
|
|
43
|
+
*
|
|
44
|
+
* Injected rather than read here, because the read used to be
|
|
45
|
+
* `testAdminUserId(request)` — `http/test-actor-carrier`, which
|
|
46
|
+
* `contracts/host-package.md` §1.4j classifies **A**: the file exists to
|
|
47
|
+
* narrow this repository's test-harness Fastify augmentation, and an
|
|
48
|
+
* installed package has no relationship to that harness. It also answered
|
|
49
|
+
* `undefined` in production for every request, because nothing under `src/`
|
|
50
|
+
* writes `request.testActor`.
|
|
51
|
+
*
|
|
52
|
+
* Both composition roots supply it from `adminContextResolver`, which reads
|
|
53
|
+
* the production actor and throws 401 for a non-admin. Every call site sits
|
|
54
|
+
* behind `requireAdmin(...)`, so the actor is an admin by the time it runs.
|
|
55
|
+
*/
|
|
56
|
+
resolveAdminUserId: (request: FastifyRequest) => string;
|
|
57
|
+
}
|
|
58
|
+
export declare function registerCreditLimitsRoutes(app: FastifyInstance, deps: CreditLimitsDeps): Promise<void>;
|
|
59
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAIL,KAAK,uBAAuB,EAC7B,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAG7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB,EAAE,kBAAkB,CAAC;IACvC;;;;;;OAMG;IACH,uBAAuB,EAAE,uBAAuB,CAAC;IACjD,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,YAAY,EAAE,mBAAmB,CAAC;IAClC,sBAAsB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAC/C,iBAAiB,EAAE,MAAM,CAAC;QAC1B,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF;;;;;;;;;;;;;;;OAeG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC;CACzD;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CAyJf"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ERROR_CODES, adjustCreditLimitRequestSchema, grantCreditLimitRequestSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { isOrgInScope } from '@endora-commerce/platform/tenancy';
|
|
4
|
+
export async function registerCreditLimitsRoutes(app, deps) {
|
|
5
|
+
const { creditLimitService, organizationDetailsPort, requireCustomer, requireAdmin, resolveCustomerContext, resolveAdminUserId, } = deps;
|
|
6
|
+
/** Resolve organization names for the given ids (read-only, missing ⇒ absent). */
|
|
7
|
+
const loadOrgNames = async (ids) => {
|
|
8
|
+
const unique = [...new Set(ids)].filter(Boolean);
|
|
9
|
+
if (unique.length === 0)
|
|
10
|
+
return new Map();
|
|
11
|
+
const orgs = await organizationDetailsPort.findByIds(unique);
|
|
12
|
+
return new Map(orgs.map((o) => [o.id, o.name]));
|
|
13
|
+
};
|
|
14
|
+
app.get('/api/v1/me/credit-limit', { preHandler: requireCustomer }, async (request) => {
|
|
15
|
+
const ctx = resolveCustomerContext(request);
|
|
16
|
+
const limit = await creditLimitService.getForOrganization(ctx.organizationId);
|
|
17
|
+
if (!limit) {
|
|
18
|
+
throw new HttpError(404, ERROR_CODES.CREDIT_LIMIT_NOT_GRANTED, 'Credit limit has not been granted for this organization.');
|
|
19
|
+
}
|
|
20
|
+
const reservations = await creditLimitService.listActiveReservations(limit.id);
|
|
21
|
+
return { data: serializeView(limit, reservations) };
|
|
22
|
+
});
|
|
23
|
+
app.get('/api/v1/admin/credit-limits', { preHandler: requireAdmin('credit_limits:manage') }, async () => {
|
|
24
|
+
const rows = await creditLimitService.listAll();
|
|
25
|
+
const names = await loadOrgNames(rows.map((l) => l.organizationId));
|
|
26
|
+
const data = await Promise.all(rows.map(async (l) => {
|
|
27
|
+
const reservations = await creditLimitService.listActiveReservations(l.id);
|
|
28
|
+
return serializeView(l, reservations, names.get(l.organizationId) ?? null);
|
|
29
|
+
}));
|
|
30
|
+
return { data };
|
|
31
|
+
});
|
|
32
|
+
app.post('/api/v1/admin/organizations/:id/credit-limit', {
|
|
33
|
+
preHandler: requireAdmin('credit_limits:manage'),
|
|
34
|
+
schema: { body: grantCreditLimitRequestSchema },
|
|
35
|
+
}, async (request, reply) => {
|
|
36
|
+
const body = grantCreditLimitRequestSchema.parse(request.body);
|
|
37
|
+
// Feature 050 — inserts are not reachable by the org column filter, so gate
|
|
38
|
+
// the target org explicitly; out-of-scope responds as "not granted" (FR-008).
|
|
39
|
+
if (!isOrgInScope(request.params.id)) {
|
|
40
|
+
throw new HttpError(404, ERROR_CODES.CREDIT_LIMIT_NOT_GRANTED, 'Credit limit has not been granted for this organization.');
|
|
41
|
+
}
|
|
42
|
+
const existing = await creditLimitService.getForOrganization(request.params.id);
|
|
43
|
+
if (existing) {
|
|
44
|
+
throw new HttpError(409, ERROR_CODES.CREDIT_LIMIT_ALREADY_GRANTED, 'A credit limit is already granted; use PATCH to adjust it.');
|
|
45
|
+
}
|
|
46
|
+
const limit = await creditLimitService.grant({
|
|
47
|
+
organizationId: request.params.id,
|
|
48
|
+
grantedAmount: body.grantedAmount,
|
|
49
|
+
currency: body.currency,
|
|
50
|
+
grantedByAdminUserId: resolveAdminUserId(request),
|
|
51
|
+
});
|
|
52
|
+
reply.status(201);
|
|
53
|
+
const names = await loadOrgNames([request.params.id]);
|
|
54
|
+
return { data: serializeView(limit, [], names.get(request.params.id) ?? null) };
|
|
55
|
+
});
|
|
56
|
+
app.get('/api/v1/admin/organizations/:id/credit-limit', { preHandler: requireAdmin('credit_limits:manage') }, async (request) => {
|
|
57
|
+
const limit = await creditLimitService.getForOrganization(request.params.id);
|
|
58
|
+
if (!limit) {
|
|
59
|
+
throw new HttpError(404, ERROR_CODES.CREDIT_LIMIT_NOT_GRANTED, 'Credit limit has not been granted for this organization.');
|
|
60
|
+
}
|
|
61
|
+
const reservations = await creditLimitService.listActiveReservations(limit.id);
|
|
62
|
+
const names = await loadOrgNames([request.params.id]);
|
|
63
|
+
return { data: serializeView(limit, reservations, names.get(request.params.id) ?? null) };
|
|
64
|
+
});
|
|
65
|
+
app.patch('/api/v1/admin/organizations/:id/credit-limit', {
|
|
66
|
+
preHandler: requireAdmin('credit_limits:manage'),
|
|
67
|
+
schema: { body: adjustCreditLimitRequestSchema },
|
|
68
|
+
}, async (request) => {
|
|
69
|
+
const body = adjustCreditLimitRequestSchema.parse(request.body);
|
|
70
|
+
// Feature 050 — gate the target org (see POST grant above).
|
|
71
|
+
if (!isOrgInScope(request.params.id)) {
|
|
72
|
+
throw new HttpError(404, ERROR_CODES.CREDIT_LIMIT_NOT_GRANTED, 'Credit limit has not been granted for this organization.');
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await creditLimitService.adjust({
|
|
76
|
+
organizationId: request.params.id,
|
|
77
|
+
grantedAmount: body.grantedAmount,
|
|
78
|
+
...(body.allowOverAllocation !== undefined
|
|
79
|
+
? { allowOverAllocation: body.allowOverAllocation }
|
|
80
|
+
: {}),
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
throw new HttpError(409, ERROR_CODES.ADJUSTMENT_BELOW_ACTIVE, 'Cannot reduce credit limit below the sum of active reservations.');
|
|
84
|
+
}
|
|
85
|
+
const reservations = await creditLimitService.listActiveReservations(result.limit.id);
|
|
86
|
+
const names = await loadOrgNames([request.params.id]);
|
|
87
|
+
return { data: serializeView(result.limit, reservations, names.get(request.params.id) ?? null) };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof Error && err.message === 'CREDIT_LIMIT_NOT_GRANTED') {
|
|
91
|
+
throw new HttpError(404, ERROR_CODES.CREDIT_LIMIT_NOT_GRANTED, 'Credit limit has not been granted for this organization.');
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function serializeView(limit, reservations, organizationName) {
|
|
98
|
+
const granted = Number(limit.grantedAmount);
|
|
99
|
+
const reservedSum = reservations.reduce((acc, r) => acc + Number(r.amount), 0);
|
|
100
|
+
return {
|
|
101
|
+
organizationId: limit.organizationId,
|
|
102
|
+
organizationName: organizationName ?? null,
|
|
103
|
+
grantedAmount: granted,
|
|
104
|
+
availableAmount: granted - reservedSum,
|
|
105
|
+
currency: limit.currency,
|
|
106
|
+
activeReservations: reservations.map((r) => ({
|
|
107
|
+
orderId: r.orderId,
|
|
108
|
+
amount: Number(r.amount),
|
|
109
|
+
createdAt: r.createdAt.toISOString(),
|
|
110
|
+
})),
|
|
111
|
+
grantedAt: limit.createdAt.toISOString(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CreditLimitReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* `creditLimitReadPort` — the membership read `organizations` needs to resolve
|
|
5
|
+
* an inherited limit (feature 077, D-87).
|
|
6
|
+
*
|
|
7
|
+
* `OrganizationInheritanceService.creditOwner` walks an ancestor chain and has
|
|
8
|
+
* to know which of those organisations hold a row here. It selected from this
|
|
9
|
+
* module's table to find out — a statement naming no import specifier, so the
|
|
10
|
+
* boundary compiled and returned rows whatever state this module was in.
|
|
11
|
+
*
|
|
12
|
+
* A class rather than a closure in `backend/index.ts` for the reason
|
|
13
|
+
* `OrganizationDetailsService` is one: several suites construct the consumer by
|
|
14
|
+
* hand and have to supply what the container supplies, and the point of a cut
|
|
15
|
+
* is that the **owner's own** implementation answers the question — a stub that
|
|
16
|
+
* agrees with the test's expectations cannot show it.
|
|
17
|
+
*/
|
|
18
|
+
export declare class CreditLimitReadService implements CreditLimitReadPort {
|
|
19
|
+
private readonly emFactory;
|
|
20
|
+
constructor(emFactory: () => EntityManager);
|
|
21
|
+
organizationsWithLimit(organizationIds: readonly string[]): Promise<string[]>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=credit-limit-read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credit-limit-read.d.ts","sourceRoot":"","sources":["../../../src/backend/services/credit-limit-read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE;;;;;;;;;;;;;;GAcG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,sBAAsB,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAcpF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CreditLimit } from '../entities/credit-limit.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* `creditLimitReadPort` — the membership read `organizations` needs to resolve
|
|
4
|
+
* an inherited limit (feature 077, D-87).
|
|
5
|
+
*
|
|
6
|
+
* `OrganizationInheritanceService.creditOwner` walks an ancestor chain and has
|
|
7
|
+
* to know which of those organisations hold a row here. It selected from this
|
|
8
|
+
* module's table to find out — a statement naming no import specifier, so the
|
|
9
|
+
* boundary compiled and returned rows whatever state this module was in.
|
|
10
|
+
*
|
|
11
|
+
* A class rather than a closure in `backend/index.ts` for the reason
|
|
12
|
+
* `OrganizationDetailsService` is one: several suites construct the consumer by
|
|
13
|
+
* hand and have to supply what the container supplies, and the point of a cut
|
|
14
|
+
* is that the **owner's own** implementation answers the question — a stub that
|
|
15
|
+
* agrees with the test's expectations cannot show it.
|
|
16
|
+
*/
|
|
17
|
+
export class CreditLimitReadService {
|
|
18
|
+
emFactory;
|
|
19
|
+
constructor(emFactory) {
|
|
20
|
+
this.emFactory = emFactory;
|
|
21
|
+
}
|
|
22
|
+
async organizationsWithLimit(organizationIds) {
|
|
23
|
+
if (organizationIds.length === 0)
|
|
24
|
+
return [];
|
|
25
|
+
const rows = await this.emFactory().find(CreditLimit, { organizationId: { $in: [...organizationIds] } },
|
|
26
|
+
// Load-bearing rather than defensive: the holder is by definition an
|
|
27
|
+
// ancestor outside the caller's tenant scope, which is the whole point of
|
|
28
|
+
// the inheritance, so `@OrgScoped` would answer the empty set for every
|
|
29
|
+
// descendant and the feature would silently stop working.
|
|
30
|
+
{ filters: { org: false } });
|
|
31
|
+
// Ids, not rows. The consumer gets a value it cannot mutate and flush.
|
|
32
|
+
return rows.map((row) => row.organizationId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=credit-limit-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credit-limit-read.js","sourceRoot":"","sources":["../../../src/backend/services/credit-limit-read.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAEjE;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,sBAAsB;IACJ;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,sBAAsB,CAAC,eAAkC;QAC7D,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CACtC,WAAW,EACX,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,eAAe,CAAC,EAAE,EAAE;QACjD,qEAAqE;QACrE,0EAA0E;QAC1E,wEAAwE;QACxE,0DAA0D;QAC1D,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CAC5B,CAAC;QACF,uEAAuE;QACvE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC/C,CAAC;CACF"}
|