17 Commits

Author SHA1 Message Date
53a3be1df4 feat(saas): coordinate tenant deployment with fenced recovery 2026-09-11 19:38:02 +08:00
56dc13b000 feat(saas): coordinate tenant deployment with fenced recovery 2026-09-11 19:23:38 +08:00
467c03ba5d feat(saas): coordinate tenant deployment with fenced recovery 2026-09-11 19:17:58 +08:00
20a472357d feat(saas): coordinate tenant deployment with fenced recovery 2026-09-11 18:23:46 +08:00
30e7f9d7dd feat(saas): coordinate tenant deployment with fenced recovery 2026-09-11 17:54:03 +08:00
09b0e2fc39 feat(saas): coordinate tenant deployment with fenced recovery 2026-09-11 16:55:15 +08:00
0f6ebd7041 feat(saas): coordinate tenant deployment with fenced recovery 2026-09-11 16:52:18 +08:00
0c508eecef fix(saas): validate release lifecycle in live multi-tenant deployment 2026-09-11 13:57:02 +08:00
24da782dcc fix(saas): validate release lifecycle in live multi-tenant deployment 2026-09-11 13:55:04 +08:00
bcff710eb5 fix(saas): validate release lifecycle in live multi-tenant deployment 2026-09-11 12:59:58 +08:00
fd4b2f67ae feat(saas): enforce signed release evidence and verified deployments 2026-09-11 12:13:07 +08:00
b0235088ad feat(saas): enforce signed release evidence and verified deployments 2026-09-11 12:11:39 +08:00
b99588a4e4 fix: generate S2 order candidate with exact goods provider and recovery runtime 2026-09-10 18:34:10 +08:00
8d45e4670f chore: align RPC isolation runtime and order business checks 2026-09-10 17:28:32 +08:00
f4c724d984 chore: pin verified RPC 9.0.4 2026-09-10 16:32:57 +08:00
1f9e5a8068 fix: secure order workflow for lifecycle candidate 2026-09-07 18:47:05 +08:00
6e7b2ec0ba test: bind order candidate 1.0.1 to exact demo stable 2026-09-04 10:26:48 +08:00
10 changed files with 459 additions and 71 deletions

View File

@@ -1,8 +1,8 @@
{ {
"moduleCode": "order", "moduleCode": "order",
"candidateVersion": "1.0.0-rc.1", "candidateVersion": "1.0.10-rc.10",
"buildRevision": 1, "buildRevision": 10,
"buildChecksum": "5cf032fdac2b0403fbcad8b44b326a29141ba556bbbb9710ac9329b5d7c031dd", "buildChecksum": "e66f8fffeaaac05d20c00a1367159d84e64de97231f528d493789a369f3ef9f5",
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23", "schemaInputChecksum": "9811550dfb225cd89362118cd649590d3d52f9388456aaf5aee082c2399bfc5b",
"branchName": "release/1.0.0-rc.1" "branchName": "release/1.0.10-rc.10"
} }

5
bootstrap.js vendored
View File

@@ -11,10 +11,11 @@ function loadGeneratedSaasModule() {
return require('./dist/saas/generated').GeneratedSaasModule; return require('./dist/saas/generated').GeneratedSaasModule;
} }
async function start() { async function start(options = {}) {
await prepareSaasServiceHost({ const session = await prepareSaasServiceHost({
generated: loadGeneratedSaasModule(), generated: loadGeneratedSaasModule(),
}); });
if (options.verifySession) await options.verifySession(session);
requireExactReleaseEnvironment(); requireExactReleaseEnvironment();
return Bootstrap.configure({ return Bootstrap.configure({
baseDir: path.join(__dirname, 'dist'), baseDir: path.join(__dirname, 'dist'),

View File

@@ -10,17 +10,19 @@
"build": "rimraf dist && mwtsc --cleanOutDir", "build": "rimraf dist && mwtsc --cleanOutDir",
"publish": "cool saas-model finalize", "publish": "cool saas-model finalize",
"start": "node bootstrap.js", "start": "node bootstrap.js",
"start:stable": "node start-stable.js",
"test:stable": "node --test test/start-stable.test.cjs",
"test:business": "node --test test/orders-business-outcome.test.cjs",
"start:local": "cross-env NODE_ENV=local SAAS_RUNTIME_BOOTSTRAP_LOCAL_FALLBACK_ENABLED=true node bootstrap.js", "start:local": "cross-env NODE_ENV=local SAAS_RUNTIME_BOOTSTRAP_LOCAL_FALLBACK_ENABLED=true node bootstrap.js",
"verify:packages": "node scripts/verify-local-packages.cjs", "verify:packages": "node scripts/verify-local-packages.cjs",
"start:frozen": "node start-frozen.js", "start:frozen": "node start-frozen.js"
"start:stable": "node start-stable.js",
"test:stable": "node --test test/start-stable.test.cjs"
}, },
"coolSaas": { "coolSaas": {
"lifecycle": { "lifecycle": {
"dev": "pnpm run build && pnpm run start:local", "dev": "pnpm run build && pnpm run start:local",
"test": "pnpm run typecheck", "test": "pnpm run typecheck && pnpm run test:stable && pnpm run test:business",
"build": "pnpm run build" "build": "pnpm run build",
"artifactDir": "dist"
} }
}, },
"dependencies": { "dependencies": {

View File

@@ -12,6 +12,7 @@ export type OrdersAddInput = {
goods_id: string; goods_id: string;
order_no: string; order_no: string;
quantity: number; quantity: number;
remark?: string | null;
status?: string; status?: string;
unit_price: string; unit_price: string;
}; };
@@ -22,6 +23,7 @@ export type OrdersAddOutput = {
id: string; id: string;
order_no: string; order_no: string;
quantity: number; quantity: number;
remark?: string | null;
status: string; status: string;
unit_price: string; unit_price: string;
}; };
@@ -76,6 +78,7 @@ export type OrdersInfoOutput = {
id: string; id: string;
order_no: string; order_no: string;
quantity: number; quantity: number;
remark?: string | null;
status: string; status: string;
unit_price: string; unit_price: string;
}; };
@@ -98,15 +101,16 @@ export type OrdersListInput = {
}; };
}; };
export type OrdersListOutput = { export type OrdersListOutput = ({
amount: string; amount: string;
goods_id: string; goods_id: string;
id: string; id: string;
order_no: string; order_no: string;
quantity: number; quantity: number;
remark?: string | null;
status: string; status: string;
unit_price: string; unit_price: string;
}[]; })[];
export type OrdersPageInput = { export type OrdersPageInput = {
keyword?: string; keyword?: string;
@@ -128,15 +132,16 @@ export type OrdersPageInput = {
}; };
export type OrdersPageOutput = { export type OrdersPageOutput = {
list: { list: ({
amount: string; amount: string;
goods_id: string; goods_id: string;
id: string; id: string;
order_no: string; order_no: string;
quantity: number; quantity: number;
remark?: string | null;
status: string; status: string;
unit_price: string; unit_price: string;
}[]; })[];
pagination: { pagination: {
page: number; page: number;
size: number; size: number;
@@ -150,6 +155,7 @@ export type OrdersUpdateInput = {
id: string; id: string;
order_no?: string; order_no?: string;
quantity?: number; quantity?: number;
remark?: string | null;
status?: string; status?: string;
unit_price?: string; unit_price?: string;
}; };
@@ -217,6 +223,13 @@ export const Actions = {
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -227,7 +240,7 @@ export const Actions = {
"amount" "amount"
] ]
}, },
"inputChecksum": "bbaa3690f78676aeed57b00951cd36a1ae2a82c4afeb86911040fdae86da53c9", "inputChecksum": "1a63df8bd8a0a663d853603eac460eb419f44131f9ca1de36c9d91925c03c95f",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -258,6 +271,13 @@ export const Actions = {
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -270,7 +290,7 @@ export const Actions = {
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:add" "permission": "order:orders:add"
@@ -284,6 +304,7 @@ export const Actions = {
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -620,6 +641,13 @@ export const Actions = {
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -632,7 +660,7 @@ export const Actions = {
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:info" "permission": "order:orders:info"
@@ -646,6 +674,7 @@ export const Actions = {
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -775,6 +804,13 @@ export const Actions = {
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -788,7 +824,7 @@ export const Actions = {
] ]
} }
}, },
"outputChecksum": "040ec4e29e6999ab8ce2e1d9ebe4d4a5b1c09e805c10870401dbb273c9a8c9bc", "outputChecksum": "a02a513155bd8cbf6dc15471919a77f076e6225975e07a90285763bd3b481661",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:list" "permission": "order:orders:list"
@@ -802,6 +838,7 @@ export const Actions = {
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -939,6 +976,13 @@ export const Actions = {
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -982,7 +1026,7 @@ export const Actions = {
"pagination" "pagination"
] ]
}, },
"outputChecksum": "14efb96cade110fc8fef771fb439845b0892d169d9f26f0be10127b547262877", "outputChecksum": "9b8f0c856c1dec532d601d56a84c8267f591ff82b89d5750df62d6104271eeb7",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:page" "permission": "order:orders:page"
@@ -996,6 +1040,7 @@ export const Actions = {
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1043,6 +1088,13 @@ export const Actions = {
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1050,7 +1102,7 @@ export const Actions = {
], ],
"minProperties": 2 "minProperties": 2
}, },
"inputChecksum": "437195e38df6988a131ad09b0b7ee089108857a4795c75aba395d077255676fc", "inputChecksum": "acf0b47a3dd2bb4dc9f7e87399f8e0fd2300391230886283588204ba9b8c8e4c",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -1078,6 +1130,7 @@ export const Actions = {
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1125,7 +1178,7 @@ export const Crud = {
moduleCode: "order", moduleCode: "order",
tableCode: "orders", tableCode: "orders",
operations: ["add","delete","update","info","list","page"], operations: ["add","delete","update","info","list","page"],
descriptorChecksum: "b4b4c2c624d655282f336f4758e6d057082541502dfd0099ca0f520f75d973fd", descriptorChecksum: "9748697d1a60b5e0957fb58c62a8564c997397007d15a6d35b783fceee178e97",
actions: { actions: {
add: Actions.OrdersAdd, add: Actions.OrdersAdd,
delete: Actions.OrdersDelete, delete: Actions.OrdersDelete,

View File

@@ -14,15 +14,16 @@ export interface OrdersRow extends Record<string, unknown> {
unit_price: string; unit_price: string;
amount: string; amount: string;
status: string; status: string;
remark?: string | null;
} }
export class Orders extends SaasTableModel<OrdersRow> { export class Orders extends SaasTableModel<OrdersRow> {
static readonly definition = defineSaasTableModel({ static readonly definition = defineSaasTableModel({
"kind": "table", "kind": "table",
"moduleCode": "order", "moduleCode": "order",
"revision": 1, "revision": 10,
"schemaVersion": 1, "schemaVersion": 10,
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23", "schemaInputChecksum": "9811550dfb225cd89362118cd649590d3d52f9388456aaf5aee082c2399bfc5b",
"tableCode": "orders", "tableCode": "orders",
"fields": [ "fields": [
{ {
@@ -94,6 +95,16 @@ export class Orders extends SaasTableModel<OrdersRow> {
"writable": true, "writable": true,
"filterable": true, "filterable": true,
"sortable": true "sortable": true
},
{
"code": "remark",
"type": "varchar",
"nullable": true,
"primary": false,
"readable": true,
"writable": true,
"filterable": false,
"sortable": false
} }
] ]
}); });

View File

@@ -4,9 +4,9 @@ import { defineGeneratedSaasModule } from '@cool-midway/module-runtime';
export const GeneratedSaasModule = defineGeneratedSaasModule({ export const GeneratedSaasModule = defineGeneratedSaasModule({
"formatVersion": 2, "formatVersion": 2,
"moduleCode": "order", "moduleCode": "order",
"revision": 1, "revision": 10,
"schemaVersion": 1, "schemaVersion": 10,
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23", "schemaInputChecksum": "9811550dfb225cd89362118cd649590d3d52f9388456aaf5aee082c2399bfc5b",
"runtimeContractChecksum": "3d025d1b634c413449eb91164fb1f859a388d8930d2de112980a606b591befce", "runtimeContractChecksum": "44e98417f57e308e66e32fc306b3ba5793ae1140ed0465531315380970c57e4d",
"implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b" "implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b"
}); });

View File

@@ -83,10 +83,10 @@ export const SaasActions = {
"actionChecksum": "3978a7ba9a3e0f91842031fd323ec63b3816eef68cb8dc82ffad6d187de8b7ea", "actionChecksum": "3978a7ba9a3e0f91842031fd323ec63b3816eef68cb8dc82ffad6d187de8b7ea",
"outputChecksum": "fe644424ef6a9e462b56edc9f70f48bb218783c69469469d4310de48589a0df5", "outputChecksum": "fe644424ef6a9e462b56edc9f70f48bb218783c69469469d4310de48589a0df5",
"contractVersion": 1, "contractVersion": 1,
"providerReleaseId": 38, "providerReleaseId": 59,
"consumerModuleCode": "order", "consumerModuleCode": "order",
"providerModuleCode": "demo", "providerModuleCode": "demo",
"providerReleaseVersion": "1.0.0-rc.1" "providerReleaseVersion": "1.0.10-rc.10"
}), }),
GoodsReleaseStock: defineInjectedSaasAction<DemoGoodsReleaseStockInput, DemoGoodsReleaseStockOutput>({ GoodsReleaseStock: defineInjectedSaasAction<DemoGoodsReleaseStockInput, DemoGoodsReleaseStockOutput>({
"kind": "action", "kind": "action",
@@ -148,10 +148,10 @@ export const SaasActions = {
"outputChecksum": "a632b9f89518c3c17861454b74173a4880d5e8482639d3ff7f761aee4d061bd9", "outputChecksum": "a632b9f89518c3c17861454b74173a4880d5e8482639d3ff7f761aee4d061bd9",
"contractVersion": 1, "contractVersion": 1,
"commandEventType": "saas.workflow.demo.goods.releaseStock", "commandEventType": "saas.workflow.demo.goods.releaseStock",
"providerReleaseId": 38, "providerReleaseId": 59,
"consumerModuleCode": "order", "consumerModuleCode": "order",
"providerModuleCode": "demo", "providerModuleCode": "demo",
"providerReleaseVersion": "1.0.0-rc.1", "providerReleaseVersion": "1.0.10-rc.10",
"commandEventContractChecksum": "18a727b91cf9410c3b7ac14e3a8799dbbfa8937f84667bfe7e31e21e662396be" "commandEventContractChecksum": "18a727b91cf9410c3b7ac14e3a8799dbbfa8937f84667bfe7e31e21e662396be"
}), }),
GoodsReserveStock: defineInjectedSaasAction<DemoGoodsReserveStockInput, DemoGoodsReserveStockOutput>({ GoodsReserveStock: defineInjectedSaasAction<DemoGoodsReserveStockInput, DemoGoodsReserveStockOutput>({
@@ -208,10 +208,10 @@ export const SaasActions = {
"outputChecksum": "545bd8b0bf6eb8670b21497236038187ee0f1df1544a318b16a399458fcb796e", "outputChecksum": "545bd8b0bf6eb8670b21497236038187ee0f1df1544a318b16a399458fcb796e",
"contractVersion": 1, "contractVersion": 1,
"commandEventType": "saas.workflow.demo.goods.reserveStock", "commandEventType": "saas.workflow.demo.goods.reserveStock",
"providerReleaseId": 38, "providerReleaseId": 59,
"consumerModuleCode": "order", "consumerModuleCode": "order",
"providerModuleCode": "demo", "providerModuleCode": "demo",
"providerReleaseVersion": "1.0.0-rc.1", "providerReleaseVersion": "1.0.10-rc.10",
"commandEventContractChecksum": "ffbecad3a3d63b8bf92fde701dfaa11c8502a2c5f9f6bbb89b755e214024c3ae" "commandEventContractChecksum": "ffbecad3a3d63b8bf92fde701dfaa11c8502a2c5f9f6bbb89b755e214024c3ae"
}) })
} }

View File

@@ -1,11 +1,11 @@
{ {
"manifestVersion": 1, "manifestVersion": 1,
"moduleCode": "order", "moduleCode": "order",
"revision": 1, "revision": 10,
"schemaChecksum": null, "schemaChecksum": null,
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23", "schemaInputChecksum": "9811550dfb225cd89362118cd649590d3d52f9388456aaf5aee082c2399bfc5b",
"actionManifestChecksum": null, "actionManifestChecksum": null,
"runtimeContractChecksum": "3d025d1b634c413449eb91164fb1f859a388d8930d2de112980a606b591befce", "runtimeContractChecksum": "44e98417f57e308e66e32fc306b3ba5793ae1140ed0465531315380970c57e4d",
"implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b", "implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b",
"actions": [ "actions": [
"OrdersAdd", "OrdersAdd",
@@ -50,7 +50,7 @@
"list", "list",
"page" "page"
], ],
"descriptorChecksum": "b4b4c2c624d655282f336f4758e6d057082541502dfd0099ca0f520f75d973fd", "descriptorChecksum": "9748697d1a60b5e0957fb58c62a8564c997397007d15a6d35b783fceee178e97",
"actions": { "actions": {
"add": "OrdersAdd", "add": "OrdersAdd",
"delete": "OrdersDelete", "delete": "OrdersDelete",
@@ -95,6 +95,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -105,7 +112,7 @@
"amount" "amount"
] ]
}, },
"inputChecksum": "bbaa3690f78676aeed57b00951cd36a1ae2a82c4afeb86911040fdae86da53c9", "inputChecksum": "1a63df8bd8a0a663d853603eac460eb419f44131f9ca1de36c9d91925c03c95f",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -136,6 +143,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -148,7 +162,7 @@
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:add" "permission": "order:orders:add"
@@ -162,6 +176,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -487,6 +502,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -499,7 +521,7 @@
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:info" "permission": "order:orders:info"
@@ -513,6 +535,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -640,6 +663,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -653,7 +683,7 @@
] ]
} }
}, },
"outputChecksum": "040ec4e29e6999ab8ce2e1d9ebe4d4a5b1c09e805c10870401dbb273c9a8c9bc", "outputChecksum": "a02a513155bd8cbf6dc15471919a77f076e6225975e07a90285763bd3b481661",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:list" "permission": "order:orders:list"
@@ -667,6 +697,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -802,6 +833,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -845,7 +883,7 @@
"pagination" "pagination"
] ]
}, },
"outputChecksum": "14efb96cade110fc8fef771fb439845b0892d169d9f26f0be10127b547262877", "outputChecksum": "9b8f0c856c1dec532d601d56a84c8267f591ff82b89d5750df62d6104271eeb7",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:page" "permission": "order:orders:page"
@@ -859,6 +897,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -905,6 +944,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -912,7 +958,7 @@
], ],
"minProperties": 2 "minProperties": 2
}, },
"inputChecksum": "437195e38df6988a131ad09b0b7ee089108857a4795c75aba395d077255676fc", "inputChecksum": "acf0b47a3dd2bb4dc9f7e87399f8e0fd2300391230886283588204ba9b8c8e4c",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -940,6 +986,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],

View File

@@ -1,13 +1,13 @@
{ {
"formatVersion": 2, "formatVersion": 2,
"moduleCode": "order", "moduleCode": "order",
"revision": 1, "revision": 10,
"releaseVersion": null, "releaseVersion": null,
"schemaVersion": 1, "schemaVersion": 10,
"schemaInputChecksum": "1b3eec01e5b04880166e20e8e6df70b79f227129ea9165e38cfbb925be3c2d23", "schemaInputChecksum": "9811550dfb225cd89362118cd649590d3d52f9388456aaf5aee082c2399bfc5b",
"schemaChecksum": null, "schemaChecksum": null,
"actionManifestChecksum": null, "actionManifestChecksum": null,
"runtimeContractChecksum": "3d025d1b634c413449eb91164fb1f859a388d8930d2de112980a606b591befce", "runtimeContractChecksum": "44e98417f57e308e66e32fc306b3ba5793ae1140ed0465531315380970c57e4d",
"implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b", "implementationManifestChecksum": "f25647d2dd90e14d6226683d2ff67ba32132b2dfdeb861015805240a96d4635b",
"models": [ "models": [
{ {
@@ -85,6 +85,16 @@
"writable": true, "writable": true,
"filterable": true, "filterable": true,
"sortable": true "sortable": true
},
{
"code": "remark",
"type": "varchar",
"nullable": true,
"primary": false,
"readable": true,
"writable": true,
"filterable": false,
"sortable": false
} }
] ]
} }
@@ -175,6 +185,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -185,7 +202,7 @@
"amount" "amount"
] ]
}, },
"inputChecksum": "bbaa3690f78676aeed57b00951cd36a1ae2a82c4afeb86911040fdae86da53c9", "inputChecksum": "1a63df8bd8a0a663d853603eac460eb419f44131f9ca1de36c9d91925c03c95f",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -216,6 +233,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -228,7 +252,7 @@
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:add" "permission": "order:orders:add"
@@ -242,6 +266,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -308,6 +333,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -318,7 +350,7 @@
"amount" "amount"
] ]
}, },
"inputChecksum": "bbaa3690f78676aeed57b00951cd36a1ae2a82c4afeb86911040fdae86da53c9", "inputChecksum": "1a63df8bd8a0a663d853603eac460eb419f44131f9ca1de36c9d91925c03c95f",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -349,6 +381,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -361,7 +400,7 @@
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:add" "permission": "order:orders:add"
@@ -375,6 +414,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -945,6 +985,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -957,7 +1004,7 @@
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:info" "permission": "order:orders:info"
@@ -971,6 +1018,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1028,6 +1076,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1040,7 +1095,7 @@
"status" "status"
] ]
}, },
"outputChecksum": "9621cab591356c44012f30c8ba2209b1c62ad4834e1b90b8899420ad1ab1721e", "outputChecksum": "a1214f4132a2d2897384694e7f3bb8fc2c436cd6ed1d2d8f09c7515bb76538d1",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:info" "permission": "order:orders:info"
@@ -1054,6 +1109,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1184,6 +1240,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1197,7 +1260,7 @@
] ]
} }
}, },
"outputChecksum": "040ec4e29e6999ab8ce2e1d9ebe4d4a5b1c09e805c10870401dbb273c9a8c9bc", "outputChecksum": "a02a513155bd8cbf6dc15471919a77f076e6225975e07a90285763bd3b481661",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:list" "permission": "order:orders:list"
@@ -1211,6 +1274,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1336,6 +1400,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1349,7 +1420,7 @@
] ]
} }
}, },
"outputChecksum": "040ec4e29e6999ab8ce2e1d9ebe4d4a5b1c09e805c10870401dbb273c9a8c9bc", "outputChecksum": "a02a513155bd8cbf6dc15471919a77f076e6225975e07a90285763bd3b481661",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:list" "permission": "order:orders:list"
@@ -1363,6 +1434,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1501,6 +1573,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1544,7 +1623,7 @@
"pagination" "pagination"
] ]
}, },
"outputChecksum": "14efb96cade110fc8fef771fb439845b0892d169d9f26f0be10127b547262877", "outputChecksum": "9b8f0c856c1dec532d601d56a84c8267f591ff82b89d5750df62d6104271eeb7",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:page" "permission": "order:orders:page"
@@ -1558,6 +1637,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1691,6 +1771,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1734,7 +1821,7 @@
"pagination" "pagination"
] ]
}, },
"outputChecksum": "14efb96cade110fc8fef771fb439845b0892d169d9f26f0be10127b547262877", "outputChecksum": "9b8f0c856c1dec532d601d56a84c8267f591ff82b89d5750df62d6104271eeb7",
"callerPolicy": { "callerPolicy": {
"kind": "permission", "kind": "permission",
"permission": "order:orders:page" "permission": "order:orders:page"
@@ -1748,6 +1835,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1797,6 +1885,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1804,7 +1899,7 @@
], ],
"minProperties": 2 "minProperties": 2
}, },
"inputChecksum": "437195e38df6988a131ad09b0b7ee089108857a4795c75aba395d077255676fc", "inputChecksum": "acf0b47a3dd2bb4dc9f7e87399f8e0fd2300391230886283588204ba9b8c8e4c",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -1832,6 +1927,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -1902,6 +1998,13 @@
"status": { "status": {
"type": "string", "type": "string",
"maxLength": 24 "maxLength": 24
},
"remark": {
"type": [
"string",
"null"
],
"maxLength": 500
} }
}, },
"required": [ "required": [
@@ -1909,7 +2012,7 @@
], ],
"minProperties": 2 "minProperties": 2
}, },
"inputChecksum": "437195e38df6988a131ad09b0b7ee089108857a4795c75aba395d077255676fc", "inputChecksum": "acf0b47a3dd2bb4dc9f7e87399f8e0fd2300391230886283588204ba9b8c8e4c",
"outputSchema": { "outputSchema": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -1937,6 +2040,7 @@
"id", "id",
"order_no", "order_no",
"quantity", "quantity",
"remark",
"status", "status",
"unit_price" "unit_price"
], ],
@@ -2031,10 +2135,10 @@
"actionChecksum": "3978a7ba9a3e0f91842031fd323ec63b3816eef68cb8dc82ffad6d187de8b7ea", "actionChecksum": "3978a7ba9a3e0f91842031fd323ec63b3816eef68cb8dc82ffad6d187de8b7ea",
"outputChecksum": "fe644424ef6a9e462b56edc9f70f48bb218783c69469469d4310de48589a0df5", "outputChecksum": "fe644424ef6a9e462b56edc9f70f48bb218783c69469469d4310de48589a0df5",
"contractVersion": 1, "contractVersion": 1,
"providerReleaseId": 38, "providerReleaseId": 59,
"consumerModuleCode": "order", "consumerModuleCode": "order",
"providerModuleCode": "demo", "providerModuleCode": "demo",
"providerReleaseVersion": "1.0.0-rc.1" "providerReleaseVersion": "1.0.10-rc.10"
} }
}, },
{ {
@@ -2101,10 +2205,10 @@
"outputChecksum": "a632b9f89518c3c17861454b74173a4880d5e8482639d3ff7f761aee4d061bd9", "outputChecksum": "a632b9f89518c3c17861454b74173a4880d5e8482639d3ff7f761aee4d061bd9",
"contractVersion": 1, "contractVersion": 1,
"commandEventType": "saas.workflow.demo.goods.releaseStock", "commandEventType": "saas.workflow.demo.goods.releaseStock",
"providerReleaseId": 38, "providerReleaseId": 59,
"consumerModuleCode": "order", "consumerModuleCode": "order",
"providerModuleCode": "demo", "providerModuleCode": "demo",
"providerReleaseVersion": "1.0.0-rc.1", "providerReleaseVersion": "1.0.10-rc.10",
"commandEventContractChecksum": "18a727b91cf9410c3b7ac14e3a8799dbbfa8937f84667bfe7e31e21e662396be" "commandEventContractChecksum": "18a727b91cf9410c3b7ac14e3a8799dbbfa8937f84667bfe7e31e21e662396be"
} }
}, },
@@ -2166,10 +2270,10 @@
"outputChecksum": "545bd8b0bf6eb8670b21497236038187ee0f1df1544a318b16a399458fcb796e", "outputChecksum": "545bd8b0bf6eb8670b21497236038187ee0f1df1544a318b16a399458fcb796e",
"contractVersion": 1, "contractVersion": 1,
"commandEventType": "saas.workflow.demo.goods.reserveStock", "commandEventType": "saas.workflow.demo.goods.reserveStock",
"providerReleaseId": 38, "providerReleaseId": 59,
"consumerModuleCode": "order", "consumerModuleCode": "order",
"providerModuleCode": "demo", "providerModuleCode": "demo",
"providerReleaseVersion": "1.0.0-rc.1", "providerReleaseVersion": "1.0.10-rc.10",
"commandEventContractChecksum": "ffbecad3a3d63b8bf92fde701dfaa11c8502a2c5f9f6bbb89b755e214024c3ae" "commandEventContractChecksum": "ffbecad3a3d63b8bf92fde701dfaa11c8502a2c5f9f6bbb89b755e214024c3ae"
} }
} }
@@ -2209,8 +2313,8 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"providerReleaseId": 38, "providerReleaseId": 59,
"providerReleaseVersion": "1.0.0-rc.1" "providerReleaseVersion": "1.0.10-rc.10"
} }
], ],
"crud": [ "crud": [
@@ -2225,7 +2329,7 @@
"list", "list",
"page" "page"
], ],
"descriptorChecksum": "b4b4c2c624d655282f336f4758e6d057082541502dfd0099ca0f520f75d973fd", "descriptorChecksum": "9748697d1a60b5e0957fb58c62a8564c997397007d15a6d35b783fceee178e97",
"actions": { "actions": {
"add": "OrdersAdd", "add": "OrdersAdd",
"delete": "OrdersDelete", "delete": "OrdersDelete",

View File

@@ -0,0 +1,170 @@
'use strict';
// Exercise the actual TypeScript service, with only its injected CRUD/Action
// collaborators stubbed. No application bootstrap, network, database or dist
// writes are needed; generated contracts and decorators remain the real ones.
require('reflect-metadata');
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ts = require('typescript');
const { SaasBusinessReject, ModuleRuntimeError } = require('@cool-midway/module-runtime');
const sourceRoot = path.resolve(__dirname, '../src');
const previousTsLoader = require.extensions['.ts'];
let OrdersService;
try {
require.extensions['.ts'] = (module, filename) => {
assert.ok(filename.startsWith(sourceRoot + path.sep), 'only this service source tree is compiled in memory');
const compiled = ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
fileName: filename,
compilerOptions: { target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS,
experimentalDecorators: true, emitDecoratorMetadata: true, esModuleInterop: true },
});
module._compile(compiled.outputText, filename);
};
({ OrdersService } = require('../src/service/orders.ts'));
} finally {
if (previousTsLoader) require.extensions['.ts'] = previousTsLoader;
else delete require.extensions['.ts'];
}
function fixture(overrides = {}) {
const state = {
goods: { id: '24', status: 1, price: '12.50', stock: 20 },
order: { id: '27', order_no: 'UNIT-ORDER', goods_id: '24', quantity: 3, amount: '37.50', status: 'confirmed' },
calls: [], ...overrides,
};
const service = new OrdersService();
service.batchInfo = async input => { state.calls.push(['batchInfo', input]); return { list: state.goods ? [state.goods] : [] }; };
service.reserveStock = async input => { state.calls.push(['reserveStock', input]); return { goodsId: input.goodsId, remainingStock: 17 }; };
service.releaseStock = async input => { state.calls.push(['releaseStock', input]); return { goodsId: input.goodsId, currentStock: 20 }; };
service.orders = {
lock: async input => {state.calls.push(['lock',input]);return state.order;},
info: async () => assert.fail('missing-record lookup must not invoke the nonnullable info contract'),
list: async input => { state.calls.push(['list', input]); return state.order ? [state.order] : []; },
add: async input => { state.calls.push(['add', input]); return { id: '27', ...input, ...state.addResult }; },
update: async input => { state.calls.push(['update', input]); return { affected: 1 }; },
};
service.workflow = {start: async input => {state.calls.push(['workflow',input]);return {sagaId:'test-saga'};}};
return { service, state };
}
const createInput = { orderNo: 'UNIT-ORDER', goodsId: 24, quantity: 3 };
const business = code => error => error instanceof SaasBusinessReject && !(error instanceof ModuleRuntimeError) && error.code === code;
const contract = error => error instanceof ModuleRuntimeError && !(error instanceof SaasBusinessReject) &&
error.errorCode === 'MODULE_ACTION_INPUT_INVALID' && error.statusCode === 409;
const noWrites = state => assert.ok(state.calls.every(([name]) => !['reserveStock', 'releaseStock', 'add', 'update'].includes(name)));
test('the existing locked list contract authorizes ID filtering and valid empty-array results', () => {
const lock = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'saas/generated/order/saas-schema.lock.json'), 'utf8'));
const model = lock.models.find(item => item.tableCode === 'orders');
assert.equal(model.fields.find(field => field.code === 'id').filterable, true);
const list = lock.actions.find(item => item.manifestDescriptor.name === 'orders.list').manifestDescriptor;
assert.equal(list.inputSchema.properties.where.properties.id.type, 'string');
assert.equal(list.inputSchema.properties.take.minimum, 1);
assert.equal(list.outputSchema.type, 'array');
assert.ok(list.tables.some(table => table.tableCode === 'orders' && table.select === true && table.fields.includes('id')));
});
test('unlisted goods is an explicit GOODS_UNAVAILABLE business rejection before any write', async () => {
const f = fixture({ goods: { id: '24', status: 0, price: '12.50' } });
await assert.rejects(f.service.createOrder(createInput), business('GOODS_UNAVAILABLE'));
noWrites(f.state);
});
test('missing goods is GOODS_NOT_FOUND in both order creation and details', async t => {
for (const method of ['createOrder', 'detailWithGoods']) await t.test(method, async () => {
const f = fixture({ goods: null });
await assert.rejects(f.service[method](method === 'createOrder' ? createInput : { id: 27 }), business('GOODS_NOT_FOUND'));
noWrites(f.state);
});
});
test('missing order is ORDER_NOT_FOUND in details and cancellation', async t => {
for (const method of ['detailWithGoods', 'cancelOrder']) await t.test(method, async () => {
const f = fixture({ order: null });
await assert.rejects(f.service[method]({ id: 27 }), business('ORDER_NOT_FOUND'));
assert.deepEqual(f.state.calls, method === 'cancelOrder' ? [['lock',{id:'27'}]] : [['list', { where: { id: '27' }, take: 1 }]]);
noWrites(f.state);
});
});
test('malformed goods status and price remain contract errors, not business warnings', async t => {
for (const [label, patch] of [['fractional status', { status: 1.2 }], ['invalid status', { status: 'bad' }],
['invalid price', { price: '12.5oops' }], ['nonfinite price', { price: Infinity }]]) {
await t.test(label, async () => {
const f = fixture(); Object.assign(f.state.goods, patch);
await assert.rejects(f.service.createOrder(createInput), contract);
noWrites(f.state);
});
}
});
test('unsafe stored goods IDs remain contract failures before a cross-module call', async t => {
for (const id of ['0', '-1', '1.5', '9007199254740992', 'bad']) await t.test(id, async () => {
const f = fixture(); f.state.order.goods_id = id;
await assert.rejects(f.service.detailWithGoods({ id: 27 }), contract);
assert.deepEqual(f.state.calls.map(([name]) => name), ['list']);
});
});
test('invalid generated order ID or amount stays a contract error after mocked CRUD', async t => {
for (const [label, addResult] of [['unsafe order ID', { id: '9007199254740992' }], ['nonfinite amount', { amount: 'NaN' }]]) {
await t.test(label, async () => {
const f = fixture({ addResult });
await assert.rejects(f.service.createOrder(createInput), contract);
assert.deepEqual(f.state.calls.map(([name]) => name), ['batchInfo', 'add', 'workflow']);
});
}
});
test('failure to persist workflow propagates to the enclosing transaction', async () => {
const f = fixture();
const downstream = new SaasBusinessReject('INSUFFICIENT_STOCK', '库存不足');
f.service.workflow.start = async () => { throw downstream; };
await assert.rejects(f.service.createOrder(createInput), error => error === downstream);
assert.equal(f.state.calls.some(([name]) => name === 'reserveStock'), false);
});
test('list contract and database failures propagate unchanged instead of becoming ORDER_NOT_FOUND', async t => {
for (const error of [new ModuleRuntimeError('MODULE_ACTION_OUTPUT_INVALID', 'invalid list output', 500),
new ModuleRuntimeError('MODULE_ACTION_FORBIDDEN', 'list access denied', 403), new Error('database unavailable')]) {
await t.test(error.message, async () => {
const f = fixture();
f.service.orders.list = async () => { throw error; };
await assert.rejects(f.service.detailWithGoods({ id: 27 }), observed => observed === error && !(observed instanceof SaasBusinessReject));
noWrites(f.state);
});
}
});
test('creation persists pending intent and starts durable workflow without a synchronous stock write', async () => {
const f = fixture();
assert.deepEqual(await f.service.createOrder(createInput), { id: 27, orderNo: 'UNIT-ORDER', amount: 37.5, status: 'pending_reservation' });
assert.deepEqual(f.state.calls.map(([name]) => name), ['batchInfo', 'add', 'workflow']);
const flow=f.state.calls[2][1];
assert.equal(flow.type,'order.create@1');
assert.deepEqual(flow.steps[0].input,{goodsId:24,quantity:3});
assert.deepEqual(flow.outcomes.completed,{id:'27',status:'confirmed'});
assert.deepEqual(flow.outcomes.failed,{id:'27',status:'rejected'});
});
test('cancellation locks state and persists release workflow; terminal and pending repeats are inert', async () => {
const f = fixture();
assert.deepEqual(await f.service.cancelOrder({ id: 27 }), { id: 27, status: 'cancel_pending', restoredStock: 0 });
assert.deepEqual(f.state.calls.map(([name]) => name), ['lock', 'update', 'workflow']);
const repeated = fixture(); repeated.state.order.status = 'cancelled';
assert.deepEqual(await repeated.service.cancelOrder({ id: 27 }), { id: 27, status: 'cancelled', restoredStock: 0 });
assert.deepEqual(repeated.state.calls.map(([name]) => name), ['lock']);
const pending=fixture(); pending.state.order.status='cancel_pending';
assert.equal((await pending.service.cancelOrder({id:27})).status,'cancel_pending');
assert.deepEqual(pending.state.calls.map(([name])=>name),['lock']);
});
test('legacy or unconfirmed orders cannot manufacture a stock release', async () => {
for (const status of ['created','pending_reservation','rejected','manual_review']) {
const f=fixture();f.state.order.status=status;
await assert.rejects(f.service.cancelOrder({id:27}),business('ORDER_STATE_CONFLICT'));
noWrites(f.state);
}
});