@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/admin/index.d.ts +31 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +32 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/CreditLimitsPage.d.ts +4 -0
  8. package/dist/admin/pages/CreditLimitsPage.d.ts.map +1 -0
  9. package/dist/admin/pages/CreditLimitsPage.js +144 -0
  10. package/dist/admin/pages/CreditLimitsPage.js.map +1 -0
  11. package/dist/backend/entities/credit-limit-reservation.entity.d.ts +35 -0
  12. package/dist/backend/entities/credit-limit-reservation.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/credit-limit-reservation.entity.js +99 -0
  14. package/dist/backend/entities/credit-limit-reservation.entity.js.map +1 -0
  15. package/dist/backend/entities/credit-limit-return-topup.entity.d.ts +29 -0
  16. package/dist/backend/entities/credit-limit-return-topup.entity.d.ts.map +1 -0
  17. package/dist/backend/entities/credit-limit-return-topup.entity.js +71 -0
  18. package/dist/backend/entities/credit-limit-return-topup.entity.js.map +1 -0
  19. package/dist/backend/entities/credit-limit.entity.d.ts +21 -0
  20. package/dist/backend/entities/credit-limit.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/credit-limit.entity.js +66 -0
  22. package/dist/backend/entities/credit-limit.entity.js.map +1 -0
  23. package/dist/backend/index.d.ts +80 -0
  24. package/dist/backend/index.d.ts.map +1 -0
  25. package/dist/backend/index.js +112 -0
  26. package/dist/backend/index.js.map +1 -0
  27. package/dist/backend/routes.d.ts +59 -0
  28. package/dist/backend/routes.d.ts.map +1 -0
  29. package/dist/backend/routes.js +114 -0
  30. package/dist/backend/routes.js.map +1 -0
  31. package/dist/backend/services/credit-limit-read.d.ts +23 -0
  32. package/dist/backend/services/credit-limit-read.d.ts.map +1 -0
  33. package/dist/backend/services/credit-limit-read.js +35 -0
  34. package/dist/backend/services/credit-limit-read.js.map +1 -0
  35. package/dist/backend/services/credit-limit-service.d.ts +179 -0
  36. package/dist/backend/services/credit-limit-service.d.ts.map +1 -0
  37. package/dist/backend/services/credit-limit-service.js +487 -0
  38. package/dist/backend/services/credit-limit-service.js.map +1 -0
  39. package/dist/backend/services/credit-topup.d.ts +33 -0
  40. package/dist/backend/services/credit-topup.d.ts.map +1 -0
  41. package/dist/backend/services/credit-topup.js +46 -0
  42. package/dist/backend/services/credit-topup.js.map +1 -0
  43. package/dist/manifest.d.ts +169 -0
  44. package/dist/manifest.d.ts.map +1 -0
  45. package/dist/manifest.js +138 -0
  46. package/dist/manifest.js.map +1 -0
  47. package/dist/migrations/20260425T063333_credit_limits_init.d.ts +15 -0
  48. package/dist/migrations/20260425T063333_credit_limits_init.d.ts.map +1 -0
  49. package/dist/migrations/20260425T063333_credit_limits_init.js +56 -0
  50. package/dist/migrations/20260425T063333_credit_limits_init.js.map +1 -0
  51. package/dist/migrations/20260817T201111_credit_limits_return_topups.d.ts +17 -0
  52. package/dist/migrations/20260817T201111_credit_limits_return_topups.d.ts.map +1 -0
  53. package/dist/migrations/20260817T201111_credit_limits_return_topups.js +36 -0
  54. package/dist/migrations/20260817T201111_credit_limits_return_topups.js.map +1 -0
  55. package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.d.ts +44 -0
  56. package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.d.ts.map +1 -0
  57. package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.js +81 -0
  58. package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.js.map +1 -0
  59. package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.d.ts +29 -0
  60. package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.d.ts.map +1 -0
  61. package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.js +45 -0
  62. package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.js.map +1 -0
  63. package/dist/migrations/index.d.ts +39 -0
  64. package/dist/migrations/index.d.ts.map +1 -0
  65. package/dist/migrations/index.js +44 -0
  66. package/dist/migrations/index.js.map +1 -0
  67. package/dist/ports/index.d.ts +93 -0
  68. package/dist/ports/index.d.ts.map +1 -0
  69. package/dist/ports/index.js +2 -0
  70. package/dist/ports/index.js.map +1 -0
  71. package/docs/credit_limits.md +53 -0
  72. package/i18n/en.json +9 -0
  73. package/i18n/pl.json +9 -0
  74. package/package.json +92 -0
  75. package/tailwind.css +14 -0
@@ -0,0 +1,81 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * `credit_limit_reservations.order_id` gains the foreign key it never had
4
+ * (D-94.1, site 4 — the site the D-94 sweep found, of exactly the shape D-90
5
+ * repaired for `shipments.order_id`).
6
+ *
7
+ * `CreditLimitService.reserve({ tx })` is called from `placeOrder` with the
8
+ * placement `EntityManager`, so the `PESSIMISTIC_WRITE` on the organization's
9
+ * `credit_limits` row — or the owning ancestor's row, taken with
10
+ * `select … for update` — is held until placement commits. A separate
11
+ * transaction would leave credit consumed for an order that rolled back:
12
+ * `stock_allocations`' argument, in money. D-78 point 2 rules such a seam
13
+ * permanent and *declared*, and what makes the declaration honest is a
14
+ * constraint. The omission tracks the module boundary and nothing else — the
15
+ * same `create table` (`20260425T063333_credit_limits_init.ts:34-48`) declares
16
+ * `credit_limit_reservations_credit_limit_fk` on `credit_limit_id` and leaves
17
+ * `order_id` bare.
18
+ *
19
+ * `on delete restrict`: a reservation row is one half of a counter. Deleting
20
+ * an order under `cascade` would drop the reservation while the credit stays
21
+ * drawn. `restrict` says the only correct thing — release it first.
22
+ *
23
+ * The column accepted unconstrained values from 2026-04-25 until this
24
+ * migration, so the constraint is preceded by an explicit orphan report. Any
25
+ * orphan with `status = 'active'` has been silently consuming an
26
+ * organization's available credit, and `releaseByOrder` — which looks the
27
+ * reservation up **by order id** — cannot reach it. Export first (it is a
28
+ * money record), then delete; releasing rather than deleting does not satisfy
29
+ * the constraint, because the row still names a missing order. If the locked
30
+ * amount is non-zero, tell the organization's owner what their available
31
+ * credit was and what it now is: this is a customer-facing correction, not a
32
+ * schema chore (D-94.2, remedy 3).
33
+ *
34
+ * `credit_limits` declares `orders` in its manifest `dependencies` for this
35
+ * edge (AGENTS.md § Migrations item 4). The reverse port edge — `orders`
36
+ * resolving `creditLimitService` — moves to `acknowledgedDependencies`, which
37
+ * drops the install ordering the constraint says is backwards and keeps every
38
+ * refusal exactly as it was (D-94.3).
39
+ */
40
+ export class Migration20260818T081252CreditLimitsCreditLimitReservationOrderFk extends Migration {
41
+ async up() {
42
+ this.addSql(`
43
+ do $$
44
+ declare
45
+ orphan_count bigint;
46
+ amount_locked numeric(14,2);
47
+ sample text;
48
+ begin
49
+ select count(*),
50
+ coalesce(sum(r."amount") filter (where r."status" = 'active'), 0)
51
+ into orphan_count, amount_locked
52
+ from "credit_limit_reservations" r
53
+ left join "orders" o on o."id" = r."order_id"
54
+ where o."id" is null;
55
+
56
+ if orphan_count > 0 then
57
+ select string_agg(x."order_id"::text, ', ') into sample
58
+ from (
59
+ select distinct r."order_id"
60
+ from "credit_limit_reservations" r
61
+ left join "orders" o on o."id" = r."order_id"
62
+ where o."id" is null
63
+ limit 10
64
+ ) x;
65
+ raise exception
66
+ 'credit_limit_reservations_order_fk cannot be added: % orphaned credit_limit_reservations.order_id value(s) reference no orders row, holding % in still-active reservations releaseByOrder cannot reach (first ten: %). Export those money records, delete them, tell each affected organization what their available credit was and what it now is, then re-run the migration.',
67
+ orphan_count, amount_locked, sample;
68
+ end if;
69
+ end $$;
70
+ `);
71
+ this.addSql(`
72
+ alter table "credit_limit_reservations"
73
+ add constraint "credit_limit_reservations_order_fk" foreign key ("order_id")
74
+ references "orders" ("id") on delete restrict;
75
+ `);
76
+ }
77
+ async down() {
78
+ this.addSql(`alter table "credit_limit_reservations" drop constraint if exists "credit_limit_reservations_order_fk";`);
79
+ }
80
+ }
81
+ //# sourceMappingURL=20260818T081252_credit_limits_credit_limit_reservation_order_fk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260818T081252_credit_limits_credit_limit_reservation_order_fk.js","sourceRoot":"","sources":["../../src/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,OAAO,iEAAkE,SAAQ,SAAS;IACrF,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4BX,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CACT,yGAAyG,CAC1G,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,29 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * `credit_limit_reservations.reserving_organization_id` — who drew the credit,
4
+ * recorded by this module instead of asked of `orders` (feature 075).
5
+ *
6
+ * The `independent_default` inheritance mode sums "the active reservations this
7
+ * descendant has consumed", and until now the only record of which descendant
8
+ * that was lived in somebody else's table: the sum ran
9
+ * `join orders o on o.id = r.order_id where o.organization_id = ?`, inside the
10
+ * placement transaction, while a `PESSIMISTIC_WRITE` was held on the owning
11
+ * organization's credit row. The value it read is one this module already has
12
+ * in hand — `reserve` is called with the reserving organization, and placement
13
+ * writes the same value onto the order — so the join was buying nothing but a
14
+ * cross-module reach the boundary check had to carry as debt.
15
+ *
16
+ * The backfill is the one place that still needs `orders`, and it is the right
17
+ * place for it: a migration naming another module's table is ordered by the
18
+ * dependency graph (this module declares `orders`, which
19
+ * `credit_limit_reservations_order_fk` already obliges) rather than executed on
20
+ * a hot money path.
21
+ *
22
+ * `not null` holds because that foreign key does: `on delete restrict` against
23
+ * `orders.id` means every reservation row has an order to take the value from.
24
+ */
25
+ export declare class Migration20260821T140323CreditLimitsReservationReservingOrganization extends Migration {
26
+ up(): Promise<void>;
27
+ down(): Promise<void>;
28
+ }
29
+ //# sourceMappingURL=20260821T140323_credit_limits_reservation_reserving_organization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260821T140323_credit_limits_reservation_reserving_organization.d.ts","sourceRoot":"","sources":["../../src/migrations/20260821T140323_credit_limits_reservation_reserving_organization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,oEAAqE,SAAQ,SAAS;IAClF,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAQrC"}
@@ -0,0 +1,45 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * `credit_limit_reservations.reserving_organization_id` — who drew the credit,
4
+ * recorded by this module instead of asked of `orders` (feature 075).
5
+ *
6
+ * The `independent_default` inheritance mode sums "the active reservations this
7
+ * descendant has consumed", and until now the only record of which descendant
8
+ * that was lived in somebody else's table: the sum ran
9
+ * `join orders o on o.id = r.order_id where o.organization_id = ?`, inside the
10
+ * placement transaction, while a `PESSIMISTIC_WRITE` was held on the owning
11
+ * organization's credit row. The value it read is one this module already has
12
+ * in hand — `reserve` is called with the reserving organization, and placement
13
+ * writes the same value onto the order — so the join was buying nothing but a
14
+ * cross-module reach the boundary check had to carry as debt.
15
+ *
16
+ * The backfill is the one place that still needs `orders`, and it is the right
17
+ * place for it: a migration naming another module's table is ordered by the
18
+ * dependency graph (this module declares `orders`, which
19
+ * `credit_limit_reservations_order_fk` already obliges) rather than executed on
20
+ * a hot money path.
21
+ *
22
+ * `not null` holds because that foreign key does: `on delete restrict` against
23
+ * `orders.id` means every reservation row has an order to take the value from.
24
+ */
25
+ export class Migration20260821T140323CreditLimitsReservationReservingOrganization extends Migration {
26
+ async up() {
27
+ this.addSql(`alter table "credit_limit_reservations" add column "reserving_organization_id" uuid null;`);
28
+ this.addSql(`
29
+ update "credit_limit_reservations" r
30
+ set "reserving_organization_id" = o."organization_id"
31
+ from "orders" o
32
+ where o."id" = r."order_id";
33
+ `);
34
+ this.addSql(`alter table "credit_limit_reservations" alter column "reserving_organization_id" set not null;`);
35
+ this.addSql(`
36
+ create index "credit_limit_reservations_reserving_organization_id_index"
37
+ on "credit_limit_reservations" ("reserving_organization_id");
38
+ `);
39
+ }
40
+ async down() {
41
+ this.addSql(`drop index if exists "credit_limit_reservations_reserving_organization_id_index";`);
42
+ this.addSql(`alter table "credit_limit_reservations" drop column "reserving_organization_id";`);
43
+ }
44
+ }
45
+ //# sourceMappingURL=20260821T140323_credit_limits_reservation_reserving_organization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260821T140323_credit_limits_reservation_reserving_organization.js","sourceRoot":"","sources":["../../src/migrations/20260821T140323_credit_limits_reservation_reserving_organization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,oEAAqE,SAAQ,SAAS;IACxF,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CACT,2FAA2F,CAC5F,CAAC;QACF,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,gGAAgG,CACjG,CAAC;QACF,IAAI,CAAC,MAAM,CAAC;;;KAGX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CACT,mFAAmF,CACpF,CAAC;QACF,IAAI,CAAC,MAAM,CACT,kFAAkF,CACnF,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent — *"the `./migrations` export of
8
+ * @endora-commerce/mod-credit-limits exports no 'migrations' array"* (D-168).
9
+ *
10
+ * **Four classes, listed in ascending timestamp, which orders this module's own
11
+ * migrations and nothing else** (feature 081). Where this block sits relative
12
+ * to every other module's is decided by the manifest `dependencies` graph:
13
+ * `credit_limits` declares `orders`, and
14
+ * `…T081252_…_credit_limit_reservation_order_fk` is why — it adds
15
+ * `credit_limit_reservations_order_fk` (`credit_limit_reservations.order_id` ->
16
+ * `orders.id`, `on delete restrict`), so `orders`' tables must already exist
17
+ * when this block runs. A foreign key needs the **table**, never the owner's
18
+ * entity class (D-169), which is what lets that constraint stand while
19
+ * `./backend` publishes no entity class by name.
20
+ *
21
+ * The **named** exports stay, and the asymmetry with `./backend` — which
22
+ * publishes an array and no named class (D-168) — is deliberate.
23
+ * `db/migrations-registry.generated.ts` imports each class by name from this
24
+ * specifier and hands it to `migration('credit_limits', …)`, and a migration
25
+ * class name is contract in a way an entity class name is not:
26
+ * `mikro_orm_migrations` persists it, so it is a string every already-migrated
27
+ * database holds.
28
+ *
29
+ * A class that is in neither the array nor the barrel is a migration that does
30
+ * not run: `migration:pending` reports nothing pending and the first symptom is
31
+ * a query against a table nobody created.
32
+ */
33
+ import { Migration20260425T063333CreditLimitsInit } from './20260425T063333_credit_limits_init.js';
34
+ import { Migration20260817T201111CreditLimitsReturnTopups } from './20260817T201111_credit_limits_return_topups.js';
35
+ import { Migration20260818T081252CreditLimitsCreditLimitReservationOrderFk } from './20260818T081252_credit_limits_credit_limit_reservation_order_fk.js';
36
+ import { Migration20260821T140323CreditLimitsReservationReservingOrganization } from './20260821T140323_credit_limits_reservation_reserving_organization.js';
37
+ export declare const migrations: (typeof Migration20260425T063333CreditLimitsInit)[];
38
+ export { Migration20260425T063333CreditLimitsInit, Migration20260817T201111CreditLimitsReturnTopups, Migration20260818T081252CreditLimitsCreditLimitReservationOrderFk, Migration20260821T140323CreditLimitsReservationReservingOrganization, };
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,wCAAwC,EAAE,MAAM,yCAAyC,CAAC;AACnG,OAAO,EAAE,gDAAgD,EAAE,MAAM,kDAAkD,CAAC;AACpH,OAAO,EAAE,iEAAiE,EAAE,MAAM,sEAAsE,CAAC;AACzJ,OAAO,EAAE,oEAAoE,EAAE,MAAM,uEAAuE,CAAC;AAE7J,eAAO,MAAM,UAAU,qDAKtB,CAAC;AAEF,OAAO,EACL,wCAAwC,EACxC,gDAAgD,EAChD,iEAAiE,EACjE,oEAAoE,GACrE,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent — *"the `./migrations` export of
8
+ * @endora-commerce/mod-credit-limits exports no 'migrations' array"* (D-168).
9
+ *
10
+ * **Four classes, listed in ascending timestamp, which orders this module's own
11
+ * migrations and nothing else** (feature 081). Where this block sits relative
12
+ * to every other module's is decided by the manifest `dependencies` graph:
13
+ * `credit_limits` declares `orders`, and
14
+ * `…T081252_…_credit_limit_reservation_order_fk` is why — it adds
15
+ * `credit_limit_reservations_order_fk` (`credit_limit_reservations.order_id` ->
16
+ * `orders.id`, `on delete restrict`), so `orders`' tables must already exist
17
+ * when this block runs. A foreign key needs the **table**, never the owner's
18
+ * entity class (D-169), which is what lets that constraint stand while
19
+ * `./backend` publishes no entity class by name.
20
+ *
21
+ * The **named** exports stay, and the asymmetry with `./backend` — which
22
+ * publishes an array and no named class (D-168) — is deliberate.
23
+ * `db/migrations-registry.generated.ts` imports each class by name from this
24
+ * specifier and hands it to `migration('credit_limits', …)`, and a migration
25
+ * class name is contract in a way an entity class name is not:
26
+ * `mikro_orm_migrations` persists it, so it is a string every already-migrated
27
+ * database holds.
28
+ *
29
+ * A class that is in neither the array nor the barrel is a migration that does
30
+ * not run: `migration:pending` reports nothing pending and the first symptom is
31
+ * a query against a table nobody created.
32
+ */
33
+ import { Migration20260425T063333CreditLimitsInit } from './20260425T063333_credit_limits_init.js';
34
+ import { Migration20260817T201111CreditLimitsReturnTopups } from './20260817T201111_credit_limits_return_topups.js';
35
+ import { Migration20260818T081252CreditLimitsCreditLimitReservationOrderFk } from './20260818T081252_credit_limits_credit_limit_reservation_order_fk.js';
36
+ import { Migration20260821T140323CreditLimitsReservationReservingOrganization } from './20260821T140323_credit_limits_reservation_reserving_organization.js';
37
+ export const migrations = [
38
+ Migration20260425T063333CreditLimitsInit,
39
+ Migration20260817T201111CreditLimitsReturnTopups,
40
+ Migration20260818T081252CreditLimitsCreditLimitReservationOrderFk,
41
+ Migration20260821T140323CreditLimitsReservationReservingOrganization,
42
+ ];
43
+ export { Migration20260425T063333CreditLimitsInit, Migration20260817T201111CreditLimitsReturnTopups, Migration20260818T081252CreditLimitsCreditLimitReservationOrderFk, Migration20260821T140323CreditLimitsReservationReservingOrganization, };
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,wCAAwC,EAAE,MAAM,yCAAyC,CAAC;AACnG,OAAO,EAAE,gDAAgD,EAAE,MAAM,kDAAkD,CAAC;AACpH,OAAO,EAAE,iEAAiE,EAAE,MAAM,sEAAsE,CAAC;AACzJ,OAAO,EAAE,oEAAoE,EAAE,MAAM,uEAAuE,CAAC;AAE7J,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,wCAAwC;IACxC,gDAAgD;IAChD,iEAAiE;IACjE,oEAAoE;CACrE,CAAC;AAEF,OAAO,EACL,wCAAwC,EACxC,gDAAgD,EAChD,iEAAiE,EACjE,oEAAoE,GACrE,CAAC"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The `./ports` subpath — the port interfaces this module publishes, and
3
+ * **nothing that exists at runtime** (layout contract R8, D-169).
4
+ *
5
+ * `tsc` compiles this file to `export {};`, and that emitted empty module is the
6
+ * `default` condition's target. A `types`-only `exports` entry type-checks and
7
+ * then answers `ERR_PACKAGE_PATH_NOT_EXPORTED` to any consumer whose toolchain
8
+ * emits the import, which is every consumer that cannot prove the import is a
9
+ * type. The implementation stays in `../backend/services/credit-limit-service.ts`
10
+ * and is reached through the container name `creditLimitService`, never through
11
+ * this subpath.
12
+ *
13
+ * No entity class leaves by this door either, type-only included: D-168 shut
14
+ * that door on `./backend` precisely so that a stranger's
15
+ * `import type { CreditLimit } from '<pkg>/backend'` is a compile error in the
16
+ * stranger's own tree, and erasure would make re-opening it free at runtime and
17
+ * permanent at compile time.
18
+ *
19
+ * `backend/test/unit/packages/module-package-ports-surface.test.ts` holds this
20
+ * file to all of that, over the real package.
21
+ */
22
+ import type { EntityManager } from '@mikro-orm/postgresql';
23
+ /**
24
+ * What order placement asks of a credit limit, typed by its owner (D-94.5).
25
+ *
26
+ * `orders` used to declare this interface itself, in `order-service.ts`, and
27
+ * `CreditLimitService` satisfied it structurally. That is the shape D-77
28
+ * rejected: `lazyPort<T>` is an unchecked cast, so with `T` on the consumer's
29
+ * side **nothing verifies that the provider still satisfies it**. The
30
+ * declaration lives here, and `orders` imports it as a type from
31
+ * `@endora-commerce/mod-credit-limits/ports`.
32
+ *
33
+ * It stays **out of `@endora-commerce/contracts`**: `reserve` takes the caller's MikroORM
34
+ * `EntityManager`, and FR-034 forbids a MikroORM type there — `admin` and
35
+ * `storefront` both compile that package, which is why it holds zero
36
+ * `@mikro-orm` imports. That is what qualifies this interface for `./ports`
37
+ * rather than for the contracts package: the test is not *"is this a real
38
+ * published port"* but *"does this signature stop the interface living in
39
+ * `packages/contracts`"* (D-171). `quote_requests`' two ports are real published
40
+ * ports and fail that test, being contract DTOs end to end.
41
+ *
42
+ * The seam itself is not a defect in the design, it *is* the design. `reserve`
43
+ * holds a `PESSIMISTIC_WRITE` on the organization's `credit_limits` row — or the
44
+ * owning ancestor's, taken with `select … for update` — and the lock has to be
45
+ * held until the order commits; a second transaction would leave credit consumed
46
+ * for a placement that then rolled back. `credit_limit_reservations_order_fk`
47
+ * (`credit_limit_reservations.order_id` -> `orders.id`, `on delete restrict`) is
48
+ * what says so in the schema. A foreign key needs the **table** and never the
49
+ * class (D-169), so that constraint stands while `./backend` publishes no entity
50
+ * class by name.
51
+ */
52
+ export interface CreditLimitPort {
53
+ /**
54
+ * Reserve `amount` against the organization's available credit.
55
+ *
56
+ * `tx` is **required** (D-94.5). It has one caller, `placeOrder`, which
57
+ * always passes its own `EntityManager`; the optional shape is what let the
58
+ * same method double as a standalone transaction, and that is a lie about
59
+ * the seam — the reservation is not separable from the placement it belongs
60
+ * to.
61
+ */
62
+ reserve(input: {
63
+ organizationId: string;
64
+ orderId: string;
65
+ amount: number;
66
+ currency: string;
67
+ tx: EntityManager;
68
+ }): Promise<{
69
+ ok: true;
70
+ reservationId: string;
71
+ availableAmountAfter: number;
72
+ } | {
73
+ ok: false;
74
+ code: 'LIMIT_INSUFFICIENT';
75
+ availableAmount: number;
76
+ } | {
77
+ ok: false;
78
+ code: 'CREDIT_LIMIT_NOT_GRANTED';
79
+ } | {
80
+ ok: false;
81
+ code: 'CURRENCY_MISMATCH';
82
+ }>;
83
+ /**
84
+ * Release the reservation an order holds. Runs in its own transaction: the
85
+ * order is committed by the time an invoice is paid, a cancellation lands or
86
+ * an admin revokes.
87
+ */
88
+ releaseByOrder(input: {
89
+ orderId: string;
90
+ reason: 'invoice_paid' | 'order_cancelled' | 'admin_revocation';
91
+ }): Promise<unknown>;
92
+ }
93
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE;QACb,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,EAAE,EAAE,aAAa,CAAC;KACnB,GAAG,OAAO,CACP;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,oBAAoB,EAAE,MAAM,CAAA;KAAE,GACjE;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,oBAAoB,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAClE;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,0BAA0B,CAAA;KAAE,GAC/C;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,mBAAmB,CAAA;KAAE,CAC3C,CAAC;IACF;;;;OAIG;IACH,cAAc,CAAC,KAAK,EAAE;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,cAAc,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;KACjE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: credit_limits
3
+ description: Credit-limit grant + atomic reservation
4
+ ---
5
+
6
+ # `credit_limits`
7
+
8
+ The Credit Limit payment method. Admins grant a per-Organization limit;
9
+ Customers consume it at checkout. Reservations are atomic and idempotently
10
+ released on invoice paid or order cancellation.
11
+
12
+ ## Public surface
13
+
14
+ All admin routes are gated by the `credit_limits:manage` permission.
15
+
16
+ | Verb + Path | Audience | Purpose |
17
+ | --- | --- | --- |
18
+ | `GET /api/v1/me/credit-limit` | customer | View granted limit + currently reserved (404 `CREDIT_LIMIT_NOT_GRANTED` if none) |
19
+ | `GET /api/v1/admin/credit-limits` | admin | Roster of every granted limit with active reservations |
20
+ | `GET /api/v1/admin/organizations/:id/credit-limit` | admin | One organization's limit + reservations |
21
+ | `POST /api/v1/admin/organizations/:id/credit-limit` | admin | Grant initial limit |
22
+ | `PATCH /api/v1/admin/organizations/:id/credit-limit` | admin | Adjust amount (rejects below active reservations unless `allowOverAllocation`) |
23
+
24
+ Storefront UX: the Account profile and Checkout page render a
25
+ `CreditLimitWidget` (granted / available / reservation breakdown) when
26
+ the limit exists; `credit_limit`-kind payment methods are filtered out
27
+ of Checkout when no limit is granted or the cart total exceeds the
28
+ available credit.
29
+
30
+ ## Concurrency model
31
+
32
+ `CreditLimitService.reserve()` opens a `SELECT … FOR UPDATE` on the
33
+ `credit_limits` row, then inserts the reservation. Two simultaneous orders
34
+ that would together exceed the limit are serialized; one succeeds and the
35
+ other receives `409 LIMIT_INSUFFICIENT`. See
36
+ `backend/test/contract/credit_limits/concurrent-race.test.ts`.
37
+
38
+ ## Entities
39
+
40
+ `CreditLimit`, `CreditLimitReservation` (status: `active | released`).
41
+ Index on `(credit_limit_id, status)` keeps the available-balance query fast.
42
+
43
+ ## Events emitted
44
+
45
+ `credit_limit.granted.v1`, `credit_limit.adjusted.v1`,
46
+ `credit_limit.reservation_released.v1`.
47
+
48
+ ## Extension points
49
+
50
+ - **Release reasons** — `releaseByOrder(reason)` accepts
51
+ `'invoice_paid'` and `'order_cancelled'` today. New reasons (e.g.
52
+ `'manual_override'`) are added by extending the enum and wiring the
53
+ caller.
package/i18n/en.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "errors.ADJUSTMENT_BELOW_ACTIVE": "The credit limit cannot be lowered below the amount that active reservations are already holding. Release or settle those reservations first, or set a higher amount.",
3
+ "errors.CREDIT_LIMIT_ALREADY_GRANTED": "This organization already has a credit limit. Adjust the existing limit instead of granting a second one.",
4
+ "errors.CREDIT_LIMIT_NOT_GRANTED": "No credit limit has been granted for this organization.",
5
+ "errors.LIMIT_INSUFFICIENT": "The available credit limit does not cover this order. Reduce the order or choose a different payment method.",
6
+ "nav.creditLimits.label": "Credit limits",
7
+ "actions.openCreditLimits.label": "Credit limits",
8
+ "actions.openCreditLimits.description": "Customer credit limits & balances"
9
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "errors.ADJUSTMENT_BELOW_ACTIVE": "Nie można obniżyć limitu kredytowego poniżej kwoty, którą blokują aktywne rezerwacje. Najpierw zwolnij lub rozlicz te rezerwacje albo ustaw wyższą kwotę.",
3
+ "errors.CREDIT_LIMIT_ALREADY_GRANTED": "Ta organizacja ma już przyznany limit kredytowy. Zmień istniejący limit zamiast przyznawać drugi.",
4
+ "errors.CREDIT_LIMIT_NOT_GRANTED": "Tej organizacji nie przyznano limitu kredytowego.",
5
+ "errors.LIMIT_INSUFFICIENT": "Dostępny limit kredytowy nie pokrywa wartości tego zamówienia. Zmniejsz zamówienie albo wybierz inną metodę płatności.",
6
+ "nav.creditLimits.label": "Limity kredytowe",
7
+ "actions.openCreditLimits.label": "Limity kredytowe",
8
+ "actions.openCreditLimits.description": "Limity kredytowe i salda klientów"
9
+ }
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@endora-commerce/mod-credit-limits",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Per-organization credit limits and credit-check enforcement at checkout.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "credit_limits"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/credit_limits"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./ports": {
34
+ "types": "./dist/ports/index.d.ts",
35
+ "default": "./dist/ports/index.js"
36
+ },
37
+ "./admin": {
38
+ "types": "./dist/admin/index.d.ts",
39
+ "default": "./dist/admin/index.js"
40
+ },
41
+ "./tailwind.css": "./tailwind.css",
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "i18n",
47
+ "docs",
48
+ "tailwind.css"
49
+ ],
50
+ "engines": {
51
+ "node": ">=22.18.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@mikro-orm/core": "^6",
55
+ "@mikro-orm/migrations": "^6",
56
+ "@mikro-orm/postgresql": "^6",
57
+ "fastify": "^5",
58
+ "react": "^19",
59
+ "@endora-commerce/admin-kit": "0.100.0",
60
+ "@endora-commerce/contracts": "0.100.0",
61
+ "@endora-commerce/platform": "0.100.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@endora-commerce/admin-kit": {
65
+ "optional": true
66
+ },
67
+ "react": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "@fastify/type-provider-zod": "^1.0.0",
73
+ "@mikro-orm/core": "^6.6.13",
74
+ "@mikro-orm/migrations": "^6.6.13",
75
+ "@mikro-orm/postgresql": "^6.6.13",
76
+ "@types/node": "^22.9.0",
77
+ "@types/react": "^19.2.14",
78
+ "fastify": "^5.12.5",
79
+ "react": "^19.2.5",
80
+ "typescript": "^5.9.3",
81
+ "vitest": "^4.1.11",
82
+ "@endora-commerce/admin-kit": "0.100.0",
83
+ "@endora-commerce/contracts": "0.100.0",
84
+ "@endora-commerce/platform": "0.100.0"
85
+ },
86
+ "scripts": {
87
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
88
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
89
+ "lint": "eslint src",
90
+ "test": "vitest run"
91
+ }
92
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-credit-limits — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";