@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,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
|
package/dist/migrations/20260818T081252_credit_limits_credit_limit_reservation_order_fk.js.map
ADDED
|
@@ -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
|
package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.d.ts.map
ADDED
|
@@ -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
|
package/dist/migrations/20260821T140323_credit_limits_reservation_reserving_organization.js.map
ADDED
|
@@ -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 @@
|
|
|
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";
|