@tailor-platform/erp-kit 0.0.1 → 0.1.1
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +196 -28
- package/dist/cli.js +914 -0
- package/package.json +67 -8
- package/schemas/app-compose/actors.yml +34 -0
- package/schemas/app-compose/business-flow.yml +50 -0
- package/schemas/app-compose/requirements.yml +33 -0
- package/schemas/app-compose/resolver.yml +47 -0
- package/schemas/app-compose/screen.yml +81 -0
- package/schemas/app-compose/story.yml +67 -0
- package/schemas/module/command.yml +52 -0
- package/schemas/module/feature.yml +58 -0
- package/schemas/module/model.yml +70 -0
- package/schemas/module/module.yml +50 -0
- package/skills/1-module-docs/SKILL.md +111 -0
- package/skills/1-module-docs/references/structure.md +22 -0
- package/skills/2-module-feature-breakdown/SKILL.md +72 -0
- package/skills/2-module-feature-breakdown/references/commands.md +48 -0
- package/skills/2-module-feature-breakdown/references/models.md +29 -0
- package/skills/2-module-feature-breakdown/references/structure.md +22 -0
- package/skills/3-module-doc-review/SKILL.md +236 -0
- package/skills/3-module-doc-review/references/commands.md +54 -0
- package/skills/3-module-doc-review/references/models.md +29 -0
- package/skills/3-module-doc-review/references/testing.md +37 -0
- package/skills/4-module-tdd-implementation/SKILL.md +74 -0
- package/skills/4-module-tdd-implementation/references/commands.md +45 -0
- package/skills/4-module-tdd-implementation/references/db-relations.md +69 -0
- package/skills/4-module-tdd-implementation/references/errors.md +7 -0
- package/skills/4-module-tdd-implementation/references/exports.md +8 -0
- package/skills/4-module-tdd-implementation/references/models.md +30 -0
- package/skills/4-module-tdd-implementation/references/structure.md +22 -0
- package/skills/4-module-tdd-implementation/references/testing.md +37 -0
- package/skills/5-module-implementation-review/SKILL.md +408 -0
- package/skills/5-module-implementation-review/references/commands.md +45 -0
- package/skills/5-module-implementation-review/references/errors.md +7 -0
- package/skills/5-module-implementation-review/references/exports.md +8 -0
- package/skills/5-module-implementation-review/references/models.md +30 -0
- package/skills/5-module-implementation-review/references/testing.md +29 -0
- package/skills/app-compose-1-requirement-analysis/SKILL.md +89 -0
- package/skills/app-compose-1-requirement-analysis/references/structure.md +27 -0
- package/skills/app-compose-2-requirements-breakdown/SKILL.md +95 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-detailview.md +106 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-form.md +139 -0
- package/skills/app-compose-2-requirements-breakdown/references/screen-listview.md +153 -0
- package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
- package/skills/app-compose-3-doc-review/SKILL.md +116 -0
- package/skills/app-compose-3-doc-review/references/structure.md +27 -0
- package/skills/app-compose-4-design-mock/SKILL.md +256 -0
- package/skills/app-compose-4-design-mock/references/component.md +50 -0
- package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
- package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
- package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
- package/skills/app-compose-4-design-mock/references/structure.md +27 -0
- package/skills/app-compose-5-design-mock-review/SKILL.md +290 -0
- package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
- package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
- package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
- package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
- package/skills/app-compose-6-implementation-spec/SKILL.md +127 -0
- package/skills/app-compose-6-implementation-spec/references/auth.md +72 -0
- package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
- package/skills/mock-scenario/SKILL.md +118 -0
- package/src/app.ts +1 -0
- package/src/cli.ts +120 -0
- package/src/commands/check.test.ts +30 -0
- package/src/commands/check.ts +66 -0
- package/src/commands/init.test.ts +88 -0
- package/src/commands/init.ts +120 -0
- package/src/commands/mock/index.ts +53 -0
- package/src/commands/mock/start.ts +179 -0
- package/src/commands/mock/validate.test.ts +185 -0
- package/src/commands/mock/validate.ts +198 -0
- package/src/commands/scaffold.test.ts +76 -0
- package/src/commands/scaffold.ts +119 -0
- package/src/commands/sync-check.test.ts +125 -0
- package/src/commands/sync-check.ts +182 -0
- package/src/integration.test.ts +63 -0
- package/src/mdschema.ts +48 -0
- package/src/mockServer.ts +55 -0
- package/src/module.ts +86 -0
- package/src/modules/accounting/.gitkeep +0 -0
- package/src/modules/coa-management/.gitkeep +0 -0
- package/src/modules/inventory/.gitkeep +0 -0
- package/src/modules/manufacturing/.gitkeep +0 -0
- package/src/modules/primitives/README.md +39 -0
- package/src/modules/primitives/command/activateCategory.test.ts +75 -0
- package/src/modules/primitives/command/activateCategory.ts +50 -0
- package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
- package/src/modules/primitives/command/activateCurrency.ts +50 -0
- package/src/modules/primitives/command/activateUnit.test.ts +53 -0
- package/src/modules/primitives/command/activateUnit.ts +50 -0
- package/src/modules/primitives/command/convertAmount.test.ts +275 -0
- package/src/modules/primitives/command/convertAmount.ts +126 -0
- package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
- package/src/modules/primitives/command/convertQuantity.ts +73 -0
- package/src/modules/primitives/command/createCategory.test.ts +126 -0
- package/src/modules/primitives/command/createCategory.ts +89 -0
- package/src/modules/primitives/command/createCurrency.test.ts +191 -0
- package/src/modules/primitives/command/createCurrency.ts +77 -0
- package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
- package/src/modules/primitives/command/createExchangeRate.ts +91 -0
- package/src/modules/primitives/command/createUnit.test.ts +214 -0
- package/src/modules/primitives/command/createUnit.ts +88 -0
- package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
- package/src/modules/primitives/command/deactivateCategory.ts +62 -0
- package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
- package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
- package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
- package/src/modules/primitives/command/deactivateUnit.ts +62 -0
- package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
- package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
- package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
- package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
- package/src/modules/primitives/db/currency.ts +30 -0
- package/src/modules/primitives/db/exchangeRate.ts +28 -0
- package/src/modules/primitives/db/unit.ts +32 -0
- package/src/modules/primitives/db/uomCategory.ts +32 -0
- package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
- package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
- package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
- package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
- package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
- package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
- package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
- package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
- package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
- package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
- package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
- package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
- package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
- package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
- package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
- package/src/modules/primitives/docs/features/uom-categories.md +52 -0
- package/src/modules/primitives/docs/models/Currency.md +45 -0
- package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
- package/src/modules/primitives/docs/models/Unit.md +46 -0
- package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
- package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
- package/src/modules/primitives/index.ts +40 -0
- package/src/modules/primitives/lib/errors.ts +138 -0
- package/src/modules/primitives/lib/types.ts +20 -0
- package/src/modules/primitives/module.ts +66 -0
- package/src/modules/primitives/permissions.ts +18 -0
- package/src/modules/primitives/tailor.config.ts +11 -0
- package/src/modules/primitives/testing/fixtures.ts +161 -0
- package/src/modules/product-management/.gitkeep +0 -0
- package/src/modules/purchase/.gitkeep +0 -0
- package/src/modules/sales/.gitkeep +0 -0
- package/src/modules/shared/createContext.test.ts +39 -0
- package/src/modules/shared/createContext.ts +15 -0
- package/src/modules/shared/defineCommand.test.ts +42 -0
- package/src/modules/shared/defineCommand.ts +19 -0
- package/src/modules/shared/definePermissions.test.ts +146 -0
- package/src/modules/shared/definePermissions.ts +94 -0
- package/src/modules/shared/entityTypes.ts +15 -0
- package/src/modules/shared/errors.ts +22 -0
- package/src/modules/shared/index.ts +1 -0
- package/src/modules/shared/internal.ts +13 -0
- package/src/modules/shared/requirePermission.test.ts +47 -0
- package/src/modules/shared/requirePermission.ts +8 -0
- package/src/modules/shared/types.ts +4 -0
- package/src/modules/supplier-management/.gitkeep +0 -0
- package/src/modules/supplier-portal/.gitkeep +0 -0
- package/src/modules/testing/index.ts +120 -0
- package/src/modules/user-management/README.md +38 -0
- package/src/modules/user-management/command/activateUser.test.ts +112 -0
- package/src/modules/user-management/command/activateUser.ts +67 -0
- package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
- package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
- package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
- package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
- package/src/modules/user-management/command/createPermission.test.ts +143 -0
- package/src/modules/user-management/command/createPermission.ts +66 -0
- package/src/modules/user-management/command/createRole.test.ts +115 -0
- package/src/modules/user-management/command/createRole.ts +52 -0
- package/src/modules/user-management/command/createUser.test.ts +198 -0
- package/src/modules/user-management/command/createUser.ts +85 -0
- package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
- package/src/modules/user-management/command/deactivateUser.ts +67 -0
- package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
- package/src/modules/user-management/command/logAuditEvent.ts +59 -0
- package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
- package/src/modules/user-management/command/reactivateUser.ts +67 -0
- package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
- package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
- package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
- package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
- package/src/modules/user-management/db/auditEvent.ts +47 -0
- package/src/modules/user-management/db/permission.ts +31 -0
- package/src/modules/user-management/db/role.ts +28 -0
- package/src/modules/user-management/db/rolePermission.ts +44 -0
- package/src/modules/user-management/db/user.ts +38 -0
- package/src/modules/user-management/db/userRole.ts +44 -0
- package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
- package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
- package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
- package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
- package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
- package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
- package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
- package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
- package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
- package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
- package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
- package/src/modules/user-management/docs/features/audit-trail.md +80 -0
- package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
- package/src/modules/user-management/docs/features/user-account-management.md +64 -0
- package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
- package/src/modules/user-management/docs/models/Permission.md +31 -0
- package/src/modules/user-management/docs/models/Role.md +31 -0
- package/src/modules/user-management/docs/models/RolePermission.md +33 -0
- package/src/modules/user-management/docs/models/User.md +47 -0
- package/src/modules/user-management/docs/models/UserRole.md +34 -0
- package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
- package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
- package/src/modules/user-management/generated/enums.ts +24 -0
- package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
- package/src/modules/user-management/index.ts +32 -0
- package/src/modules/user-management/lib/errors.ts +81 -0
- package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
- package/src/modules/user-management/lib/types.ts +31 -0
- package/src/modules/user-management/module.ts +77 -0
- package/src/modules/user-management/permissions.ts +15 -0
- package/src/modules/user-management/tailor.config.ts +11 -0
- package/src/modules/user-management/testing/fixtures.ts +98 -0
- package/src/schemas.ts +25 -0
- package/src/testing.ts +10 -0
- package/src/util.ts +3 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import {
|
|
4
|
+
CurrencyNotFoundError,
|
|
5
|
+
InactiveCurrencyError,
|
|
6
|
+
InvalidExchangeRateError,
|
|
7
|
+
SameCurrencyPairError,
|
|
8
|
+
} from "../lib/errors";
|
|
9
|
+
import { permissions } from "../permissions";
|
|
10
|
+
|
|
11
|
+
interface CreateExchangeRateInput {
|
|
12
|
+
sourceCurrencyId: string;
|
|
13
|
+
targetCurrencyId: string;
|
|
14
|
+
rate: number;
|
|
15
|
+
effectiveDate: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Function: createExchangeRate
|
|
20
|
+
*
|
|
21
|
+
* Establishes a new conversion ratio between a currency pair with a specific
|
|
22
|
+
* effective date. The rate specifies how many units of the target currency
|
|
23
|
+
* equal one unit of the source currency.
|
|
24
|
+
*/
|
|
25
|
+
export function makeCreateExchangeRate<CF extends Record<string, unknown>>() {
|
|
26
|
+
return defineCommand(
|
|
27
|
+
permissions.createExchangeRate,
|
|
28
|
+
async (db: DB, input: CreateExchangeRateInput & CF) => {
|
|
29
|
+
const { sourceCurrencyId, targetCurrencyId, rate, effectiveDate, ...customFields } = input;
|
|
30
|
+
|
|
31
|
+
// 1. Check source currency exists
|
|
32
|
+
const sourceCurrency = await db
|
|
33
|
+
.selectFrom("Currency")
|
|
34
|
+
.selectAll()
|
|
35
|
+
.where("id", "=", sourceCurrencyId)
|
|
36
|
+
.executeTakeFirst();
|
|
37
|
+
|
|
38
|
+
if (!sourceCurrency) {
|
|
39
|
+
throw new CurrencyNotFoundError(sourceCurrencyId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Check source currency is active
|
|
43
|
+
if (!sourceCurrency.isActive) {
|
|
44
|
+
throw new InactiveCurrencyError(sourceCurrency.code);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Check target currency exists
|
|
48
|
+
const targetCurrency = await db
|
|
49
|
+
.selectFrom("Currency")
|
|
50
|
+
.selectAll()
|
|
51
|
+
.where("id", "=", targetCurrencyId)
|
|
52
|
+
.executeTakeFirst();
|
|
53
|
+
|
|
54
|
+
if (!targetCurrency) {
|
|
55
|
+
throw new CurrencyNotFoundError(targetCurrencyId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 4. Check target currency is active
|
|
59
|
+
if (!targetCurrency.isActive) {
|
|
60
|
+
throw new InactiveCurrencyError(targetCurrency.code);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 5. Check source and target are different
|
|
64
|
+
if (sourceCurrencyId === targetCurrencyId) {
|
|
65
|
+
throw new SameCurrencyPairError(sourceCurrencyId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 6. Validate rate is positive
|
|
69
|
+
if (rate <= 0) {
|
|
70
|
+
throw new InvalidExchangeRateError(rate);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 7. Create exchange rate
|
|
74
|
+
const exchangeRate = await db
|
|
75
|
+
.insertInto("ExchangeRate")
|
|
76
|
+
.values({
|
|
77
|
+
...(customFields as Record<string, unknown>),
|
|
78
|
+
sourceCurrencyId,
|
|
79
|
+
targetCurrencyId,
|
|
80
|
+
rate,
|
|
81
|
+
effectiveDate,
|
|
82
|
+
createdAt: new Date(),
|
|
83
|
+
updatedAt: null,
|
|
84
|
+
})
|
|
85
|
+
.returningAll()
|
|
86
|
+
.executeTakeFirst();
|
|
87
|
+
|
|
88
|
+
return { exchangeRate: exchangeRate! };
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb } from "../../testing/index";
|
|
3
|
+
import { InsufficientPermissionError, type CommandContext } from "../../shared/internal";
|
|
4
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
5
|
+
import {
|
|
6
|
+
CategoryNotActiveError,
|
|
7
|
+
DuplicateUnitSymbolError,
|
|
8
|
+
InvalidConversionFactorError,
|
|
9
|
+
InvalidRoundingPrecisionError,
|
|
10
|
+
UoMCategoryNotFoundError,
|
|
11
|
+
} from "../lib/errors";
|
|
12
|
+
import { baseUoMCategory, baseUnitKg } from "../testing/fixtures";
|
|
13
|
+
import { makeCreateUnit } from "./createUnit";
|
|
14
|
+
|
|
15
|
+
const createUnit = makeCreateUnit();
|
|
16
|
+
|
|
17
|
+
describe("createUnit", () => {
|
|
18
|
+
const ctx: CommandContext = { actorId: "test-actor", permissions: ["primitives:createUnit"] };
|
|
19
|
+
|
|
20
|
+
const inactiveCategory = {
|
|
21
|
+
...baseUoMCategory,
|
|
22
|
+
isActive: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Error cases
|
|
26
|
+
it("throws when category doesn't exist", async () => {
|
|
27
|
+
const { db, spies } = createMockDb<DB>();
|
|
28
|
+
spies.select.mockReturnValue(undefined);
|
|
29
|
+
|
|
30
|
+
await expect(
|
|
31
|
+
createUnit(
|
|
32
|
+
db,
|
|
33
|
+
{
|
|
34
|
+
categoryId: "nonexistent-category",
|
|
35
|
+
name: "Ounce",
|
|
36
|
+
symbol: "oz",
|
|
37
|
+
conversionFactor: 0.0283495,
|
|
38
|
+
roundingPrecision: 2,
|
|
39
|
+
},
|
|
40
|
+
ctx,
|
|
41
|
+
),
|
|
42
|
+
).rejects.toBeInstanceOf(UoMCategoryNotFoundError);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("throws when category is inactive", async () => {
|
|
46
|
+
const { db, spies } = createMockDb<DB>();
|
|
47
|
+
spies.select.mockReturnValue(inactiveCategory);
|
|
48
|
+
|
|
49
|
+
await expect(
|
|
50
|
+
createUnit(
|
|
51
|
+
db,
|
|
52
|
+
{
|
|
53
|
+
categoryId: inactiveCategory.id,
|
|
54
|
+
name: "Ounce",
|
|
55
|
+
symbol: "oz",
|
|
56
|
+
conversionFactor: 0.0283495,
|
|
57
|
+
roundingPrecision: 2,
|
|
58
|
+
},
|
|
59
|
+
ctx,
|
|
60
|
+
),
|
|
61
|
+
).rejects.toBeInstanceOf(CategoryNotActiveError);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws when symbol already exists in category", async () => {
|
|
65
|
+
const { db, spies } = createMockDb<DB>();
|
|
66
|
+
spies.select
|
|
67
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
68
|
+
.mockReturnValueOnce(baseUnitKg); // Existing unit with same symbol
|
|
69
|
+
|
|
70
|
+
await expect(
|
|
71
|
+
createUnit(
|
|
72
|
+
db,
|
|
73
|
+
{
|
|
74
|
+
categoryId: baseUoMCategory.id,
|
|
75
|
+
name: "Kilogram Duplicate",
|
|
76
|
+
symbol: "kg",
|
|
77
|
+
conversionFactor: 1.0,
|
|
78
|
+
roundingPrecision: 2,
|
|
79
|
+
},
|
|
80
|
+
ctx,
|
|
81
|
+
),
|
|
82
|
+
).rejects.toBeInstanceOf(DuplicateUnitSymbolError);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("throws when conversion factor is not positive", async () => {
|
|
86
|
+
const { db, spies } = createMockDb<DB>();
|
|
87
|
+
spies.select
|
|
88
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
89
|
+
.mockReturnValueOnce(undefined); // No duplicate symbol
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
createUnit(
|
|
93
|
+
db,
|
|
94
|
+
{
|
|
95
|
+
categoryId: baseUoMCategory.id,
|
|
96
|
+
name: "Invalid Unit",
|
|
97
|
+
symbol: "inv",
|
|
98
|
+
conversionFactor: 0,
|
|
99
|
+
roundingPrecision: 2,
|
|
100
|
+
},
|
|
101
|
+
ctx,
|
|
102
|
+
),
|
|
103
|
+
).rejects.toBeInstanceOf(InvalidConversionFactorError);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("throws when rounding precision is negative", async () => {
|
|
107
|
+
const { db, spies } = createMockDb<DB>();
|
|
108
|
+
spies.select
|
|
109
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
110
|
+
.mockReturnValueOnce(undefined); // No duplicate symbol
|
|
111
|
+
|
|
112
|
+
await expect(
|
|
113
|
+
createUnit(
|
|
114
|
+
db,
|
|
115
|
+
{
|
|
116
|
+
categoryId: baseUoMCategory.id,
|
|
117
|
+
name: "Invalid Unit",
|
|
118
|
+
symbol: "inv",
|
|
119
|
+
conversionFactor: 1.0,
|
|
120
|
+
roundingPrecision: -1,
|
|
121
|
+
},
|
|
122
|
+
ctx,
|
|
123
|
+
),
|
|
124
|
+
).rejects.toBeInstanceOf(InvalidRoundingPrecisionError);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Success cases
|
|
128
|
+
it("creates unit successfully", async () => {
|
|
129
|
+
const { db, spies } = createMockDb<DB>();
|
|
130
|
+
const createdUnit = {
|
|
131
|
+
id: "new-unit-id",
|
|
132
|
+
name: "Ounce",
|
|
133
|
+
symbol: "oz",
|
|
134
|
+
categoryId: baseUoMCategory.id,
|
|
135
|
+
conversionFactor: 0.0283495,
|
|
136
|
+
roundingPrecision: 2,
|
|
137
|
+
isActive: true,
|
|
138
|
+
createdAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
139
|
+
updatedAt: null,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
spies.select
|
|
143
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
144
|
+
.mockReturnValueOnce(undefined); // No duplicate symbol
|
|
145
|
+
spies.insert.mockReturnValue(createdUnit);
|
|
146
|
+
|
|
147
|
+
const result = await createUnit(
|
|
148
|
+
db,
|
|
149
|
+
{
|
|
150
|
+
categoryId: baseUoMCategory.id,
|
|
151
|
+
name: "Ounce",
|
|
152
|
+
symbol: "oz",
|
|
153
|
+
conversionFactor: 0.0283495,
|
|
154
|
+
roundingPrecision: 2,
|
|
155
|
+
},
|
|
156
|
+
ctx,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(result.unit.name).toBe("Ounce");
|
|
160
|
+
expect(result.unit.symbol).toBe("oz");
|
|
161
|
+
expect(result.unit.isActive).toBe(true);
|
|
162
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("throws when permission is missing", async () => {
|
|
166
|
+
const { db } = createMockDb<DB>();
|
|
167
|
+
const denied: CommandContext = { actorId: "test-actor", permissions: [] };
|
|
168
|
+
await expect(
|
|
169
|
+
createUnit(
|
|
170
|
+
db,
|
|
171
|
+
{
|
|
172
|
+
categoryId: "cat-1",
|
|
173
|
+
name: "Test",
|
|
174
|
+
symbol: "t",
|
|
175
|
+
conversionFactor: 1,
|
|
176
|
+
roundingPrecision: 2,
|
|
177
|
+
},
|
|
178
|
+
denied,
|
|
179
|
+
),
|
|
180
|
+
).rejects.toBeInstanceOf(InsufficientPermissionError);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("passes custom fields through to insert", async () => {
|
|
184
|
+
const createUnitWithFields = makeCreateUnit<{ displayOrder: number }>();
|
|
185
|
+
const { db, spies } = createMockDb<DB>();
|
|
186
|
+
const createdUnit = {
|
|
187
|
+
...baseUnitKg,
|
|
188
|
+
id: "new-unit-id",
|
|
189
|
+
name: "Ounce",
|
|
190
|
+
symbol: "oz",
|
|
191
|
+
displayOrder: 5,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
spies.select
|
|
195
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
196
|
+
.mockReturnValueOnce(undefined); // No duplicate symbol
|
|
197
|
+
spies.insert.mockReturnValue(createdUnit);
|
|
198
|
+
|
|
199
|
+
await createUnitWithFields(
|
|
200
|
+
db,
|
|
201
|
+
{
|
|
202
|
+
categoryId: baseUoMCategory.id,
|
|
203
|
+
name: "Ounce",
|
|
204
|
+
symbol: "oz",
|
|
205
|
+
conversionFactor: 0.0283495,
|
|
206
|
+
roundingPrecision: 2,
|
|
207
|
+
displayOrder: 5,
|
|
208
|
+
},
|
|
209
|
+
ctx,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(spies.values).toHaveBeenNthCalledWith(1, expect.objectContaining({ displayOrder: 5 }));
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import {
|
|
4
|
+
CategoryNotActiveError,
|
|
5
|
+
DuplicateUnitSymbolError,
|
|
6
|
+
InvalidConversionFactorError,
|
|
7
|
+
InvalidRoundingPrecisionError,
|
|
8
|
+
UoMCategoryNotFoundError,
|
|
9
|
+
} from "../lib/errors";
|
|
10
|
+
import { permissions } from "../permissions";
|
|
11
|
+
|
|
12
|
+
interface CreateUnitInput {
|
|
13
|
+
categoryId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
symbol: string;
|
|
16
|
+
conversionFactor: number;
|
|
17
|
+
roundingPrecision: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Function: createUnit
|
|
22
|
+
*
|
|
23
|
+
* Adds a new unit of measure to an existing category with its symbol,
|
|
24
|
+
* display name, and conversion factor relative to the reference unit.
|
|
25
|
+
*/
|
|
26
|
+
export function makeCreateUnit<CF extends Record<string, unknown>>() {
|
|
27
|
+
return defineCommand(permissions.createUnit, async (db: DB, input: CreateUnitInput & CF) => {
|
|
28
|
+
const { categoryId, name, symbol, conversionFactor, roundingPrecision, ...customFields } =
|
|
29
|
+
input;
|
|
30
|
+
|
|
31
|
+
// 1. Check category exists
|
|
32
|
+
const category = await db
|
|
33
|
+
.selectFrom("UoMCategory")
|
|
34
|
+
.selectAll()
|
|
35
|
+
.where("id", "=", categoryId)
|
|
36
|
+
.executeTakeFirst();
|
|
37
|
+
|
|
38
|
+
if (!category) {
|
|
39
|
+
throw new UoMCategoryNotFoundError(categoryId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Check category is active
|
|
43
|
+
if (!category.isActive) {
|
|
44
|
+
throw new CategoryNotActiveError(categoryId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Check symbol uniqueness within category
|
|
48
|
+
const existingUnit = await db
|
|
49
|
+
.selectFrom("Unit")
|
|
50
|
+
.selectAll()
|
|
51
|
+
.where("categoryId", "=", categoryId)
|
|
52
|
+
.where("symbol", "=", symbol)
|
|
53
|
+
.executeTakeFirst();
|
|
54
|
+
|
|
55
|
+
if (existingUnit) {
|
|
56
|
+
throw new DuplicateUnitSymbolError(symbol, categoryId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Validate conversion factor
|
|
60
|
+
if (conversionFactor <= 0) {
|
|
61
|
+
throw new InvalidConversionFactorError(conversionFactor);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 5. Validate rounding precision
|
|
65
|
+
if (roundingPrecision < 0) {
|
|
66
|
+
throw new InvalidRoundingPrecisionError(roundingPrecision);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 6. Create unit
|
|
70
|
+
const unit = await db
|
|
71
|
+
.insertInto("Unit")
|
|
72
|
+
.values({
|
|
73
|
+
...(customFields as Record<string, unknown>),
|
|
74
|
+
name,
|
|
75
|
+
symbol,
|
|
76
|
+
categoryId,
|
|
77
|
+
conversionFactor,
|
|
78
|
+
roundingPrecision,
|
|
79
|
+
isActive: true,
|
|
80
|
+
createdAt: new Date(),
|
|
81
|
+
updatedAt: null,
|
|
82
|
+
})
|
|
83
|
+
.returningAll()
|
|
84
|
+
.executeTakeFirst();
|
|
85
|
+
|
|
86
|
+
return { unit: unit! };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb, testNotFound, testPermissionDenied } from "../../testing/index";
|
|
3
|
+
import { type CommandContext } from "../../shared/internal";
|
|
4
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
5
|
+
import { CategoryHasActiveUnitsError, UoMCategoryNotFoundError } from "../lib/errors";
|
|
6
|
+
import { baseUoMCategory, baseUnitKg } from "../testing/fixtures";
|
|
7
|
+
import { deactivateCategory } from "./deactivateCategory";
|
|
8
|
+
|
|
9
|
+
describe("deactivateCategory", () => {
|
|
10
|
+
const ctx: CommandContext = {
|
|
11
|
+
actorId: "test-actor",
|
|
12
|
+
permissions: ["primitives:deactivateCategory"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const inactiveCategory = {
|
|
16
|
+
...baseUoMCategory,
|
|
17
|
+
isActive: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
it(
|
|
21
|
+
"throws when category doesn't exist",
|
|
22
|
+
testNotFound(
|
|
23
|
+
deactivateCategory,
|
|
24
|
+
{ categoryId: "nonexistent-category" },
|
|
25
|
+
ctx,
|
|
26
|
+
UoMCategoryNotFoundError,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it("throws when category has active units", async () => {
|
|
31
|
+
const { db, spies } = createMockDb<DB>();
|
|
32
|
+
spies.select
|
|
33
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
34
|
+
.mockReturnValueOnce([baseUnitKg]); // Active units check
|
|
35
|
+
|
|
36
|
+
await expect(
|
|
37
|
+
deactivateCategory(
|
|
38
|
+
db,
|
|
39
|
+
{
|
|
40
|
+
categoryId: baseUoMCategory.id,
|
|
41
|
+
},
|
|
42
|
+
ctx,
|
|
43
|
+
),
|
|
44
|
+
).rejects.toBeInstanceOf(CategoryHasActiveUnitsError);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Idempotent case needs multi-mock setup, kept inline
|
|
48
|
+
it("returns category unchanged when already inactive", async () => {
|
|
49
|
+
const { db, spies } = createMockDb<DB>();
|
|
50
|
+
spies.select
|
|
51
|
+
.mockReturnValueOnce(inactiveCategory) // Category lookup
|
|
52
|
+
.mockReturnValueOnce([]); // No active units
|
|
53
|
+
|
|
54
|
+
const result = await deactivateCategory(
|
|
55
|
+
db,
|
|
56
|
+
{
|
|
57
|
+
categoryId: inactiveCategory.id,
|
|
58
|
+
},
|
|
59
|
+
ctx,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(result.category).toEqual(inactiveCategory);
|
|
63
|
+
expect(spies.update).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Success cases
|
|
67
|
+
it("deactivates active category with no active units", async () => {
|
|
68
|
+
const { db, spies } = createMockDb<DB>();
|
|
69
|
+
const deactivatedCategory = {
|
|
70
|
+
...baseUoMCategory,
|
|
71
|
+
isActive: false,
|
|
72
|
+
updatedAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
spies.select
|
|
76
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
77
|
+
.mockReturnValueOnce([]); // No active units
|
|
78
|
+
spies.update.mockReturnValue(deactivatedCategory);
|
|
79
|
+
|
|
80
|
+
const result = await deactivateCategory(
|
|
81
|
+
db,
|
|
82
|
+
{
|
|
83
|
+
categoryId: baseUoMCategory.id,
|
|
84
|
+
},
|
|
85
|
+
ctx,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(result.category.isActive).toBe(false);
|
|
89
|
+
expect(result.category.updatedAt).not.toBeNull();
|
|
90
|
+
expect(spies.update).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it(
|
|
94
|
+
"throws when permission is missing",
|
|
95
|
+
testPermissionDenied(deactivateCategory, { categoryId: "cat-1" }),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import { CategoryHasActiveUnitsError, UoMCategoryNotFoundError } from "../lib/errors";
|
|
4
|
+
import { permissions } from "../permissions";
|
|
5
|
+
|
|
6
|
+
export interface DeactivateCategoryInput {
|
|
7
|
+
categoryId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Function: deactivateCategory
|
|
12
|
+
*
|
|
13
|
+
* Disables a UoM category from being used in new product assignments
|
|
14
|
+
* while preserving historical data. All units must be deactivated first.
|
|
15
|
+
*/
|
|
16
|
+
export const deactivateCategory = defineCommand(
|
|
17
|
+
permissions.deactivateCategory,
|
|
18
|
+
async (db: DB, input: DeactivateCategoryInput) => {
|
|
19
|
+
// 1. Find category by ID
|
|
20
|
+
const category = await db
|
|
21
|
+
.selectFrom("UoMCategory")
|
|
22
|
+
.selectAll()
|
|
23
|
+
.where("id", "=", input.categoryId)
|
|
24
|
+
.executeTakeFirst();
|
|
25
|
+
|
|
26
|
+
// 2. If not found, throw error
|
|
27
|
+
if (!category) {
|
|
28
|
+
throw new UoMCategoryNotFoundError(input.categoryId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. Check for active units in this category
|
|
32
|
+
const activeUnits = await db
|
|
33
|
+
.selectFrom("Unit")
|
|
34
|
+
.selectAll()
|
|
35
|
+
.where("categoryId", "=", input.categoryId)
|
|
36
|
+
.where("isActive", "=", true)
|
|
37
|
+
.execute();
|
|
38
|
+
|
|
39
|
+
if (activeUnits.length > 0) {
|
|
40
|
+
throw new CategoryHasActiveUnitsError(input.categoryId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 4. If already inactive, return category (idempotent)
|
|
44
|
+
if (!category.isActive) {
|
|
45
|
+
return { category };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 5. Update isActive = false
|
|
49
|
+
const updatedCategory = await db
|
|
50
|
+
.updateTable("UoMCategory")
|
|
51
|
+
.set({
|
|
52
|
+
isActive: false,
|
|
53
|
+
updatedAt: new Date(),
|
|
54
|
+
})
|
|
55
|
+
.where("id", "=", input.categoryId)
|
|
56
|
+
.returningAll()
|
|
57
|
+
.executeTakeFirst();
|
|
58
|
+
|
|
59
|
+
// 6. Return updated category
|
|
60
|
+
return { category: updatedCategory! };
|
|
61
|
+
},
|
|
62
|
+
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createMockDb,
|
|
4
|
+
testNotFound,
|
|
5
|
+
testPermissionDenied,
|
|
6
|
+
testIdempotent,
|
|
7
|
+
} from "../../testing/index";
|
|
8
|
+
import { type CommandContext } from "../../shared/internal";
|
|
9
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
10
|
+
import { CannotDeactivateBaseCurrencyError, CurrencyNotFoundError } from "../lib/errors";
|
|
11
|
+
import { baseCurrencyEUR, baseCurrencyUSD, inactiveCurrency } from "../testing/fixtures";
|
|
12
|
+
import { deactivateCurrency } from "./deactivateCurrency";
|
|
13
|
+
|
|
14
|
+
describe("deactivateCurrency", () => {
|
|
15
|
+
const ctx: CommandContext = {
|
|
16
|
+
actorId: "test-actor",
|
|
17
|
+
permissions: ["primitives:deactivateCurrency"],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
it(
|
|
21
|
+
"throws when currency doesn't exist",
|
|
22
|
+
testNotFound(
|
|
23
|
+
deactivateCurrency,
|
|
24
|
+
{ currencyId: "nonexistent-currency" },
|
|
25
|
+
ctx,
|
|
26
|
+
CurrencyNotFoundError,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it("throws when attempting to deactivate base currency", async () => {
|
|
31
|
+
const { db, spies } = createMockDb<DB>();
|
|
32
|
+
spies.select.mockReturnValue(baseCurrencyUSD); // USD is base currency
|
|
33
|
+
|
|
34
|
+
await expect(
|
|
35
|
+
deactivateCurrency(
|
|
36
|
+
db,
|
|
37
|
+
{
|
|
38
|
+
currencyId: baseCurrencyUSD.id,
|
|
39
|
+
},
|
|
40
|
+
ctx,
|
|
41
|
+
),
|
|
42
|
+
).rejects.toBeInstanceOf(CannotDeactivateBaseCurrencyError);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it(
|
|
46
|
+
"returns currency unchanged when already inactive",
|
|
47
|
+
testIdempotent(
|
|
48
|
+
deactivateCurrency,
|
|
49
|
+
{ currencyId: inactiveCurrency.id },
|
|
50
|
+
ctx,
|
|
51
|
+
inactiveCurrency,
|
|
52
|
+
"currency",
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Success cases
|
|
57
|
+
it("deactivates active non-base currency", async () => {
|
|
58
|
+
const { db, spies } = createMockDb<DB>();
|
|
59
|
+
const deactivatedCurrency = {
|
|
60
|
+
...baseCurrencyEUR,
|
|
61
|
+
isActive: false,
|
|
62
|
+
updatedAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
spies.select.mockReturnValue(baseCurrencyEUR); // EUR is not base currency
|
|
66
|
+
spies.update.mockReturnValue(deactivatedCurrency);
|
|
67
|
+
|
|
68
|
+
const result = await deactivateCurrency(
|
|
69
|
+
db,
|
|
70
|
+
{
|
|
71
|
+
currencyId: baseCurrencyEUR.id,
|
|
72
|
+
},
|
|
73
|
+
ctx,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result.currency.isActive).toBe(false);
|
|
77
|
+
expect(result.currency.updatedAt).not.toBeNull();
|
|
78
|
+
expect(spies.update).toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it(
|
|
82
|
+
"throws when permission is missing",
|
|
83
|
+
testPermissionDenied(deactivateCurrency, { currencyId: "cur-1" }),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import { CannotDeactivateBaseCurrencyError, CurrencyNotFoundError } from "../lib/errors";
|
|
4
|
+
import { permissions } from "../permissions";
|
|
5
|
+
|
|
6
|
+
export interface DeactivateCurrencyInput {
|
|
7
|
+
currencyId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Function: deactivateCurrency
|
|
12
|
+
*
|
|
13
|
+
* Disables a currency from being used in new transactions while preserving
|
|
14
|
+
* historical data. Base currency cannot be deactivated.
|
|
15
|
+
*/
|
|
16
|
+
export const deactivateCurrency = defineCommand(
|
|
17
|
+
permissions.deactivateCurrency,
|
|
18
|
+
async (db: DB, input: DeactivateCurrencyInput) => {
|
|
19
|
+
// 1. Find currency by ID
|
|
20
|
+
const currency = await db
|
|
21
|
+
.selectFrom("Currency")
|
|
22
|
+
.selectAll()
|
|
23
|
+
.where("id", "=", input.currencyId)
|
|
24
|
+
.executeTakeFirst();
|
|
25
|
+
|
|
26
|
+
// 2. If not found, throw error
|
|
27
|
+
if (!currency) {
|
|
28
|
+
throw new CurrencyNotFoundError(input.currencyId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. Check if base currency
|
|
32
|
+
if (currency.isBaseCurrency) {
|
|
33
|
+
throw new CannotDeactivateBaseCurrencyError(input.currencyId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 4. If already inactive, return currency (idempotent)
|
|
37
|
+
if (!currency.isActive) {
|
|
38
|
+
return { currency };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 5. Update isActive = false
|
|
42
|
+
const updatedCurrency = await db
|
|
43
|
+
.updateTable("Currency")
|
|
44
|
+
.set({
|
|
45
|
+
isActive: false,
|
|
46
|
+
updatedAt: new Date(),
|
|
47
|
+
})
|
|
48
|
+
.where("id", "=", input.currencyId)
|
|
49
|
+
.returningAll()
|
|
50
|
+
.executeTakeFirst();
|
|
51
|
+
|
|
52
|
+
// 6. Return updated currency
|
|
53
|
+
return { currency: updatedCurrency! };
|
|
54
|
+
},
|
|
55
|
+
);
|