@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,126 @@
|
|
|
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 { DuplicateCategoryNameError } from "../lib/errors";
|
|
6
|
+
import { baseUoMCategory, baseUnitKg } from "../testing/fixtures";
|
|
7
|
+
import { makeCreateCategory } from "./createCategory";
|
|
8
|
+
|
|
9
|
+
const createCategory = makeCreateCategory();
|
|
10
|
+
|
|
11
|
+
describe("createCategory", () => {
|
|
12
|
+
const ctx: CommandContext = { actorId: "test-actor", permissions: ["primitives:createCategory"] };
|
|
13
|
+
|
|
14
|
+
// Error cases
|
|
15
|
+
it("throws when category name already exists", async () => {
|
|
16
|
+
const { db, spies } = createMockDb<DB>();
|
|
17
|
+
spies.select.mockReturnValue(baseUoMCategory);
|
|
18
|
+
|
|
19
|
+
await expect(
|
|
20
|
+
createCategory(
|
|
21
|
+
db,
|
|
22
|
+
{
|
|
23
|
+
name: baseUoMCategory.name,
|
|
24
|
+
referenceUnit: {
|
|
25
|
+
name: "Kilogram",
|
|
26
|
+
symbol: "kg",
|
|
27
|
+
roundingPrecision: 2,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
ctx,
|
|
31
|
+
),
|
|
32
|
+
).rejects.toBeInstanceOf(DuplicateCategoryNameError);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Success cases
|
|
36
|
+
it("creates category with reference unit", async () => {
|
|
37
|
+
const { db, spies } = createMockDb<DB>();
|
|
38
|
+
const createdCategory = {
|
|
39
|
+
...baseUoMCategory,
|
|
40
|
+
id: "new-category-id",
|
|
41
|
+
referenceUnitId: "new-unit-id",
|
|
42
|
+
};
|
|
43
|
+
const createdUnit = {
|
|
44
|
+
...baseUnitKg,
|
|
45
|
+
id: "new-unit-id",
|
|
46
|
+
categoryId: "new-category-id",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
spies.select.mockReturnValue(undefined); // No existing category
|
|
50
|
+
spies.insert.mockReturnValueOnce(createdCategory).mockReturnValueOnce(createdUnit);
|
|
51
|
+
spies.update.mockReturnValue({ ...createdCategory, referenceUnitId: "new-unit-id" });
|
|
52
|
+
|
|
53
|
+
const result = await createCategory(
|
|
54
|
+
db,
|
|
55
|
+
{
|
|
56
|
+
name: "Weight",
|
|
57
|
+
description: "Weight units",
|
|
58
|
+
referenceUnit: {
|
|
59
|
+
name: "Kilogram",
|
|
60
|
+
symbol: "kg",
|
|
61
|
+
roundingPrecision: 2,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
ctx,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(result.category.name).toBe("Weight");
|
|
68
|
+
expect(result.category.isActive).toBe(true);
|
|
69
|
+
expect(result.referenceUnit.conversionFactor).toBe(1.0);
|
|
70
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("throws when permission is missing", async () => {
|
|
74
|
+
const { db } = createMockDb<DB>();
|
|
75
|
+
const denied: CommandContext = { actorId: "test-actor", permissions: [] };
|
|
76
|
+
await expect(
|
|
77
|
+
createCategory(
|
|
78
|
+
db,
|
|
79
|
+
{ name: "Test", referenceUnit: { name: "Unit", symbol: "u", roundingPrecision: 2 } },
|
|
80
|
+
denied,
|
|
81
|
+
),
|
|
82
|
+
).rejects.toBeInstanceOf(InsufficientPermissionError);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("passes custom fields through to insert", async () => {
|
|
86
|
+
const createCategoryWithFields = makeCreateCategory<
|
|
87
|
+
{ priority: number },
|
|
88
|
+
{ tolerance: number }
|
|
89
|
+
>();
|
|
90
|
+
const { db, spies } = createMockDb<DB>();
|
|
91
|
+
const createdCategory = {
|
|
92
|
+
...baseUoMCategory,
|
|
93
|
+
id: "new-category-id",
|
|
94
|
+
referenceUnitId: "new-unit-id",
|
|
95
|
+
};
|
|
96
|
+
const createdUnit = {
|
|
97
|
+
...baseUnitKg,
|
|
98
|
+
id: "new-unit-id",
|
|
99
|
+
categoryId: "new-category-id",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
spies.select.mockReturnValue(undefined);
|
|
103
|
+
spies.insert.mockReturnValueOnce(createdCategory).mockReturnValueOnce(createdUnit);
|
|
104
|
+
spies.update.mockReturnValue({ ...createdCategory, referenceUnitId: "new-unit-id" });
|
|
105
|
+
|
|
106
|
+
await createCategoryWithFields(
|
|
107
|
+
db,
|
|
108
|
+
{
|
|
109
|
+
name: "Weight",
|
|
110
|
+
priority: 1,
|
|
111
|
+
referenceUnit: {
|
|
112
|
+
name: "Kilogram",
|
|
113
|
+
symbol: "kg",
|
|
114
|
+
roundingPrecision: 2,
|
|
115
|
+
tolerance: 0.01,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
ctx,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Category insert includes category custom field
|
|
122
|
+
expect(spies.values).toHaveBeenNthCalledWith(1, expect.objectContaining({ priority: 1 }));
|
|
123
|
+
// Unit insert includes unit custom field
|
|
124
|
+
expect(spies.values).toHaveBeenNthCalledWith(2, expect.objectContaining({ tolerance: 0.01 }));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import { DuplicateCategoryNameError } from "../lib/errors";
|
|
4
|
+
import { permissions } from "../permissions";
|
|
5
|
+
|
|
6
|
+
interface CreateCategoryInput<UnitCF extends Record<string, unknown>> {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
referenceUnit: {
|
|
10
|
+
name: string;
|
|
11
|
+
symbol: string;
|
|
12
|
+
roundingPrecision: number;
|
|
13
|
+
} & UnitCF;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Function: createCategory
|
|
18
|
+
*
|
|
19
|
+
* Establishes a new unit of measure category that groups related units.
|
|
20
|
+
* Creates the category with a reference unit that has conversion factor 1.0.
|
|
21
|
+
*/
|
|
22
|
+
export function makeCreateCategory<
|
|
23
|
+
CatCF extends Record<string, unknown>,
|
|
24
|
+
UnitCF extends Record<string, unknown>,
|
|
25
|
+
>() {
|
|
26
|
+
return defineCommand(
|
|
27
|
+
permissions.createCategory,
|
|
28
|
+
async (db: DB, input: CreateCategoryInput<UnitCF> & CatCF) => {
|
|
29
|
+
const { name, description, referenceUnit, ...categoryCustomFields } = input;
|
|
30
|
+
const { name: unitName, symbol, roundingPrecision, ...unitCustomFields } = referenceUnit;
|
|
31
|
+
|
|
32
|
+
// 1. Check if category name already exists
|
|
33
|
+
const existingCategory = await db
|
|
34
|
+
.selectFrom("UoMCategory")
|
|
35
|
+
.selectAll()
|
|
36
|
+
.where("name", "=", name)
|
|
37
|
+
.executeTakeFirst();
|
|
38
|
+
|
|
39
|
+
if (existingCategory) {
|
|
40
|
+
throw new DuplicateCategoryNameError(name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Create category without reference unit first
|
|
44
|
+
const category = await db
|
|
45
|
+
.insertInto("UoMCategory")
|
|
46
|
+
.values({
|
|
47
|
+
...(categoryCustomFields as Record<string, unknown>),
|
|
48
|
+
name,
|
|
49
|
+
description: description ?? null,
|
|
50
|
+
referenceUnitId: null,
|
|
51
|
+
isActive: true,
|
|
52
|
+
createdAt: new Date(),
|
|
53
|
+
updatedAt: null,
|
|
54
|
+
})
|
|
55
|
+
.returningAll()
|
|
56
|
+
.executeTakeFirst();
|
|
57
|
+
|
|
58
|
+
// 3. Create reference unit with conversion factor 1.0
|
|
59
|
+
const createdReferenceUnit = await db
|
|
60
|
+
.insertInto("Unit")
|
|
61
|
+
.values({
|
|
62
|
+
...(unitCustomFields as Record<string, unknown>),
|
|
63
|
+
name: unitName,
|
|
64
|
+
symbol,
|
|
65
|
+
categoryId: category!.id,
|
|
66
|
+
conversionFactor: 1.0,
|
|
67
|
+
roundingPrecision,
|
|
68
|
+
isActive: true,
|
|
69
|
+
createdAt: new Date(),
|
|
70
|
+
updatedAt: null,
|
|
71
|
+
})
|
|
72
|
+
.returningAll()
|
|
73
|
+
.executeTakeFirst();
|
|
74
|
+
|
|
75
|
+
// 4. Update category with reference unit ID
|
|
76
|
+
const updatedCategory = await db
|
|
77
|
+
.updateTable("UoMCategory")
|
|
78
|
+
.set({
|
|
79
|
+
referenceUnitId: createdReferenceUnit!.id,
|
|
80
|
+
updatedAt: new Date(),
|
|
81
|
+
})
|
|
82
|
+
.where("id", "=", category!.id)
|
|
83
|
+
.returningAll()
|
|
84
|
+
.executeTakeFirst();
|
|
85
|
+
|
|
86
|
+
return { category: updatedCategory!, referenceUnit: createdReferenceUnit! };
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { InsufficientPermissionError, type CommandContext } from "../../shared/internal";
|
|
3
|
+
import { createMockDb } from "../../testing/index";
|
|
4
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
5
|
+
import {
|
|
6
|
+
DuplicateCurrencyCodeError,
|
|
7
|
+
InvalidDecimalPlacesError,
|
|
8
|
+
InvalidISOCodeError,
|
|
9
|
+
} from "../lib/errors";
|
|
10
|
+
import { baseCurrencyUSD } from "../testing/fixtures";
|
|
11
|
+
import { makeCreateCurrency } from "./createCurrency";
|
|
12
|
+
|
|
13
|
+
const createCurrency = makeCreateCurrency();
|
|
14
|
+
|
|
15
|
+
describe("createCurrency", () => {
|
|
16
|
+
const ctx: CommandContext = { actorId: "test-actor", permissions: ["primitives:createCurrency"] };
|
|
17
|
+
|
|
18
|
+
// Error cases
|
|
19
|
+
it("throws when ISO code is invalid format", async () => {
|
|
20
|
+
const { db } = createMockDb<DB>();
|
|
21
|
+
|
|
22
|
+
await expect(
|
|
23
|
+
createCurrency(
|
|
24
|
+
db,
|
|
25
|
+
{
|
|
26
|
+
code: "us", // Should be 3 uppercase letters
|
|
27
|
+
name: "US Dollar",
|
|
28
|
+
symbol: "$",
|
|
29
|
+
decimalPlaces: 2,
|
|
30
|
+
},
|
|
31
|
+
ctx,
|
|
32
|
+
),
|
|
33
|
+
).rejects.toBeInstanceOf(InvalidISOCodeError);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("throws when ISO code already exists", async () => {
|
|
37
|
+
const { db, spies } = createMockDb<DB>();
|
|
38
|
+
spies.select.mockReturnValue(baseCurrencyUSD);
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
createCurrency(
|
|
42
|
+
db,
|
|
43
|
+
{
|
|
44
|
+
code: "USD",
|
|
45
|
+
name: "US Dollar",
|
|
46
|
+
symbol: "$",
|
|
47
|
+
decimalPlaces: 2,
|
|
48
|
+
},
|
|
49
|
+
ctx,
|
|
50
|
+
),
|
|
51
|
+
).rejects.toBeInstanceOf(DuplicateCurrencyCodeError);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("throws when decimal places is negative", async () => {
|
|
55
|
+
const { db, spies } = createMockDb<DB>();
|
|
56
|
+
spies.select.mockReturnValue(undefined); // No existing currency
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
createCurrency(
|
|
60
|
+
db,
|
|
61
|
+
{
|
|
62
|
+
code: "XXX",
|
|
63
|
+
name: "Test Currency",
|
|
64
|
+
symbol: "X",
|
|
65
|
+
decimalPlaces: -1,
|
|
66
|
+
},
|
|
67
|
+
ctx,
|
|
68
|
+
),
|
|
69
|
+
).rejects.toBeInstanceOf(InvalidDecimalPlacesError);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("throws when decimal places exceeds maximum", async () => {
|
|
73
|
+
const { db, spies } = createMockDb<DB>();
|
|
74
|
+
spies.select.mockReturnValue(undefined); // No existing currency
|
|
75
|
+
|
|
76
|
+
await expect(
|
|
77
|
+
createCurrency(
|
|
78
|
+
db,
|
|
79
|
+
{
|
|
80
|
+
code: "XXX",
|
|
81
|
+
name: "Test Currency",
|
|
82
|
+
symbol: "X",
|
|
83
|
+
decimalPlaces: 5,
|
|
84
|
+
},
|
|
85
|
+
ctx,
|
|
86
|
+
),
|
|
87
|
+
).rejects.toBeInstanceOf(InvalidDecimalPlacesError);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Success cases
|
|
91
|
+
it("creates first currency as base currency", async () => {
|
|
92
|
+
const { db, spies } = createMockDb<DB>();
|
|
93
|
+
const createdCurrency = {
|
|
94
|
+
id: "new-currency-id",
|
|
95
|
+
code: "USD",
|
|
96
|
+
name: "US Dollar",
|
|
97
|
+
symbol: "$",
|
|
98
|
+
decimalPlaces: 2,
|
|
99
|
+
isBaseCurrency: true,
|
|
100
|
+
isActive: true,
|
|
101
|
+
createdAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
102
|
+
updatedAt: null,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
spies.select
|
|
106
|
+
.mockReturnValueOnce(undefined) // No existing with same code
|
|
107
|
+
.mockReturnValueOnce(undefined); // No existing currencies (first currency)
|
|
108
|
+
spies.insert.mockReturnValue(createdCurrency);
|
|
109
|
+
|
|
110
|
+
const result = await createCurrency(
|
|
111
|
+
db,
|
|
112
|
+
{
|
|
113
|
+
code: "USD",
|
|
114
|
+
name: "US Dollar",
|
|
115
|
+
symbol: "$",
|
|
116
|
+
decimalPlaces: 2,
|
|
117
|
+
},
|
|
118
|
+
ctx,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result.currency.code).toBe("USD");
|
|
122
|
+
expect(result.currency.isBaseCurrency).toBe(true);
|
|
123
|
+
expect(result.currency.isActive).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("creates subsequent currency as non-base", async () => {
|
|
127
|
+
const { db, spies } = createMockDb<DB>();
|
|
128
|
+
const createdCurrency = {
|
|
129
|
+
id: "new-currency-id",
|
|
130
|
+
code: "EUR",
|
|
131
|
+
name: "Euro",
|
|
132
|
+
symbol: "€",
|
|
133
|
+
decimalPlaces: 2,
|
|
134
|
+
isBaseCurrency: false,
|
|
135
|
+
isActive: true,
|
|
136
|
+
createdAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
137
|
+
updatedAt: null,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
spies.select
|
|
141
|
+
.mockReturnValueOnce(undefined) // No existing with same code
|
|
142
|
+
.mockReturnValueOnce(baseCurrencyUSD); // Existing currency exists
|
|
143
|
+
spies.insert.mockReturnValue(createdCurrency);
|
|
144
|
+
|
|
145
|
+
const result = await createCurrency(
|
|
146
|
+
db,
|
|
147
|
+
{
|
|
148
|
+
code: "EUR",
|
|
149
|
+
name: "Euro",
|
|
150
|
+
symbol: "€",
|
|
151
|
+
decimalPlaces: 2,
|
|
152
|
+
},
|
|
153
|
+
ctx,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(result.currency.code).toBe("EUR");
|
|
157
|
+
expect(result.currency.isBaseCurrency).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("throws when permission is missing", async () => {
|
|
161
|
+
const { db } = createMockDb<DB>();
|
|
162
|
+
const denied: CommandContext = { actorId: "test-actor", permissions: [] };
|
|
163
|
+
await expect(
|
|
164
|
+
createCurrency(db, { code: "USD", name: "US Dollar", symbol: "$", decimalPlaces: 2 }, denied),
|
|
165
|
+
).rejects.toBeInstanceOf(InsufficientPermissionError);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("passes custom fields through to insert", async () => {
|
|
169
|
+
const createCurrencyWithFields = makeCreateCurrency<{ region: string }>();
|
|
170
|
+
const { db, spies } = createMockDb<DB>();
|
|
171
|
+
const createdCurrency = {
|
|
172
|
+
...baseCurrencyUSD,
|
|
173
|
+
id: "new-currency-id",
|
|
174
|
+
code: "GBP",
|
|
175
|
+
region: "Europe",
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
spies.select
|
|
179
|
+
.mockReturnValueOnce(undefined) // No existing with same code
|
|
180
|
+
.mockReturnValueOnce(undefined); // No existing currencies
|
|
181
|
+
spies.insert.mockReturnValue(createdCurrency);
|
|
182
|
+
|
|
183
|
+
await createCurrencyWithFields(
|
|
184
|
+
db,
|
|
185
|
+
{ code: "GBP", name: "British Pound", symbol: "£", decimalPlaces: 2, region: "Europe" },
|
|
186
|
+
ctx,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(spies.values).toHaveBeenNthCalledWith(1, expect.objectContaining({ region: "Europe" }));
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import {
|
|
4
|
+
DuplicateCurrencyCodeError,
|
|
5
|
+
InvalidDecimalPlacesError,
|
|
6
|
+
InvalidISOCodeError,
|
|
7
|
+
} from "../lib/errors";
|
|
8
|
+
import { permissions } from "../permissions";
|
|
9
|
+
|
|
10
|
+
interface CreateCurrencyInput {
|
|
11
|
+
code: string;
|
|
12
|
+
name: string;
|
|
13
|
+
symbol: string;
|
|
14
|
+
decimalPlaces: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ISO_CODE_PATTERN = /^[A-Z]{3}$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Function: createCurrency
|
|
21
|
+
*
|
|
22
|
+
* Establishes a new monetary unit with its ISO 4217 code, display symbol,
|
|
23
|
+
* name, and decimal precision. The first currency becomes the base currency.
|
|
24
|
+
*/
|
|
25
|
+
export function makeCreateCurrency<CF extends Record<string, unknown>>() {
|
|
26
|
+
return defineCommand(
|
|
27
|
+
permissions.createCurrency,
|
|
28
|
+
async (db: DB, input: CreateCurrencyInput & CF) => {
|
|
29
|
+
const { code, name, symbol, decimalPlaces, ...customFields } = input;
|
|
30
|
+
|
|
31
|
+
// 1. Validate ISO code format
|
|
32
|
+
if (!ISO_CODE_PATTERN.test(code)) {
|
|
33
|
+
throw new InvalidISOCodeError(code);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Check code uniqueness
|
|
37
|
+
const existingCurrency = await db
|
|
38
|
+
.selectFrom("Currency")
|
|
39
|
+
.selectAll()
|
|
40
|
+
.where("code", "=", code)
|
|
41
|
+
.executeTakeFirst();
|
|
42
|
+
|
|
43
|
+
if (existingCurrency) {
|
|
44
|
+
throw new DuplicateCurrencyCodeError(code);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Validate decimal places
|
|
48
|
+
if (decimalPlaces < 0 || decimalPlaces > 4) {
|
|
49
|
+
throw new InvalidDecimalPlacesError(decimalPlaces);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 4. Check if this is the first currency
|
|
53
|
+
const anyCurrency = await db.selectFrom("Currency").selectAll().executeTakeFirst();
|
|
54
|
+
|
|
55
|
+
const isBaseCurrency = !anyCurrency;
|
|
56
|
+
|
|
57
|
+
// 5. Create currency
|
|
58
|
+
const currency = await db
|
|
59
|
+
.insertInto("Currency")
|
|
60
|
+
.values({
|
|
61
|
+
...(customFields as Record<string, unknown>),
|
|
62
|
+
code,
|
|
63
|
+
name,
|
|
64
|
+
symbol,
|
|
65
|
+
decimalPlaces,
|
|
66
|
+
isBaseCurrency,
|
|
67
|
+
isActive: true,
|
|
68
|
+
createdAt: new Date(),
|
|
69
|
+
updatedAt: null,
|
|
70
|
+
})
|
|
71
|
+
.returningAll()
|
|
72
|
+
.executeTakeFirst();
|
|
73
|
+
|
|
74
|
+
return { currency: currency! };
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { InsufficientPermissionError, type CommandContext } from "../../shared/internal";
|
|
3
|
+
import { createMockDb } from "../../testing/index";
|
|
4
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
5
|
+
import {
|
|
6
|
+
CurrencyNotFoundError,
|
|
7
|
+
InactiveCurrencyError,
|
|
8
|
+
InvalidExchangeRateError,
|
|
9
|
+
SameCurrencyPairError,
|
|
10
|
+
} from "../lib/errors";
|
|
11
|
+
import {
|
|
12
|
+
baseCurrencyEUR,
|
|
13
|
+
baseCurrencyUSD,
|
|
14
|
+
baseExchangeRateUSDtoEUR,
|
|
15
|
+
inactiveCurrency,
|
|
16
|
+
} from "../testing/fixtures";
|
|
17
|
+
import { makeCreateExchangeRate } from "./createExchangeRate";
|
|
18
|
+
|
|
19
|
+
const createExchangeRate = makeCreateExchangeRate();
|
|
20
|
+
|
|
21
|
+
describe("createExchangeRate", () => {
|
|
22
|
+
const ctx: CommandContext = {
|
|
23
|
+
actorId: "test-actor",
|
|
24
|
+
permissions: ["primitives:createExchangeRate"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Error cases
|
|
28
|
+
it("throws when source currency doesn't exist", async () => {
|
|
29
|
+
const { db, spies } = createMockDb<DB>();
|
|
30
|
+
spies.select.mockReturnValue(undefined);
|
|
31
|
+
|
|
32
|
+
await expect(
|
|
33
|
+
createExchangeRate(
|
|
34
|
+
db,
|
|
35
|
+
{
|
|
36
|
+
sourceCurrencyId: "nonexistent-currency",
|
|
37
|
+
targetCurrencyId: baseCurrencyEUR.id,
|
|
38
|
+
rate: 0.92,
|
|
39
|
+
effectiveDate: new Date("2024-01-15"),
|
|
40
|
+
},
|
|
41
|
+
ctx,
|
|
42
|
+
),
|
|
43
|
+
).rejects.toBeInstanceOf(CurrencyNotFoundError);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("throws when source currency is inactive", async () => {
|
|
47
|
+
const { db, spies } = createMockDb<DB>();
|
|
48
|
+
spies.select.mockReturnValue(inactiveCurrency);
|
|
49
|
+
|
|
50
|
+
await expect(
|
|
51
|
+
createExchangeRate(
|
|
52
|
+
db,
|
|
53
|
+
{
|
|
54
|
+
sourceCurrencyId: inactiveCurrency.id,
|
|
55
|
+
targetCurrencyId: baseCurrencyEUR.id,
|
|
56
|
+
rate: 0.92,
|
|
57
|
+
effectiveDate: new Date("2024-01-15"),
|
|
58
|
+
},
|
|
59
|
+
ctx,
|
|
60
|
+
),
|
|
61
|
+
).rejects.toBeInstanceOf(InactiveCurrencyError);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws when target currency doesn't exist", async () => {
|
|
65
|
+
const { db, spies } = createMockDb<DB>();
|
|
66
|
+
spies.select
|
|
67
|
+
.mockReturnValueOnce(baseCurrencyUSD) // Source exists
|
|
68
|
+
.mockReturnValueOnce(undefined); // Target doesn't exist
|
|
69
|
+
|
|
70
|
+
await expect(
|
|
71
|
+
createExchangeRate(
|
|
72
|
+
db,
|
|
73
|
+
{
|
|
74
|
+
sourceCurrencyId: baseCurrencyUSD.id,
|
|
75
|
+
targetCurrencyId: "nonexistent-currency",
|
|
76
|
+
rate: 0.92,
|
|
77
|
+
effectiveDate: new Date("2024-01-15"),
|
|
78
|
+
},
|
|
79
|
+
ctx,
|
|
80
|
+
),
|
|
81
|
+
).rejects.toBeInstanceOf(CurrencyNotFoundError);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throws when target currency is inactive", async () => {
|
|
85
|
+
const { db, spies } = createMockDb<DB>();
|
|
86
|
+
spies.select
|
|
87
|
+
.mockReturnValueOnce(baseCurrencyUSD) // Source exists and active
|
|
88
|
+
.mockReturnValueOnce(inactiveCurrency); // Target is inactive
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
createExchangeRate(
|
|
92
|
+
db,
|
|
93
|
+
{
|
|
94
|
+
sourceCurrencyId: baseCurrencyUSD.id,
|
|
95
|
+
targetCurrencyId: inactiveCurrency.id,
|
|
96
|
+
rate: 0.92,
|
|
97
|
+
effectiveDate: new Date("2024-01-15"),
|
|
98
|
+
},
|
|
99
|
+
ctx,
|
|
100
|
+
),
|
|
101
|
+
).rejects.toBeInstanceOf(InactiveCurrencyError);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("throws when source and target are the same", async () => {
|
|
105
|
+
const { db, spies } = createMockDb<DB>();
|
|
106
|
+
spies.select.mockReturnValue(baseCurrencyUSD);
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
createExchangeRate(
|
|
110
|
+
db,
|
|
111
|
+
{
|
|
112
|
+
sourceCurrencyId: baseCurrencyUSD.id,
|
|
113
|
+
targetCurrencyId: baseCurrencyUSD.id,
|
|
114
|
+
rate: 1.0,
|
|
115
|
+
effectiveDate: new Date("2024-01-15"),
|
|
116
|
+
},
|
|
117
|
+
ctx,
|
|
118
|
+
),
|
|
119
|
+
).rejects.toBeInstanceOf(SameCurrencyPairError);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("throws when rate is not positive", async () => {
|
|
123
|
+
const { db, spies } = createMockDb<DB>();
|
|
124
|
+
spies.select.mockReturnValueOnce(baseCurrencyUSD).mockReturnValueOnce(baseCurrencyEUR);
|
|
125
|
+
|
|
126
|
+
await expect(
|
|
127
|
+
createExchangeRate(
|
|
128
|
+
db,
|
|
129
|
+
{
|
|
130
|
+
sourceCurrencyId: baseCurrencyUSD.id,
|
|
131
|
+
targetCurrencyId: baseCurrencyEUR.id,
|
|
132
|
+
rate: 0,
|
|
133
|
+
effectiveDate: new Date("2024-01-15"),
|
|
134
|
+
},
|
|
135
|
+
ctx,
|
|
136
|
+
),
|
|
137
|
+
).rejects.toBeInstanceOf(InvalidExchangeRateError);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Success cases
|
|
141
|
+
it("creates exchange rate successfully", async () => {
|
|
142
|
+
const { db, spies } = createMockDb<DB>();
|
|
143
|
+
const createdRate = {
|
|
144
|
+
id: "new-rate-id",
|
|
145
|
+
sourceCurrencyId: baseCurrencyUSD.id,
|
|
146
|
+
targetCurrencyId: baseCurrencyEUR.id,
|
|
147
|
+
rate: 0.92,
|
|
148
|
+
effectiveDate: new Date("2024-01-15T00:00:00.000Z"),
|
|
149
|
+
createdAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
150
|
+
updatedAt: null,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
spies.select.mockReturnValueOnce(baseCurrencyUSD).mockReturnValueOnce(baseCurrencyEUR);
|
|
154
|
+
spies.insert.mockReturnValue(createdRate);
|
|
155
|
+
|
|
156
|
+
const result = await createExchangeRate(
|
|
157
|
+
db,
|
|
158
|
+
{
|
|
159
|
+
sourceCurrencyId: baseCurrencyUSD.id,
|
|
160
|
+
targetCurrencyId: baseCurrencyEUR.id,
|
|
161
|
+
rate: 0.92,
|
|
162
|
+
effectiveDate: new Date("2024-01-15"),
|
|
163
|
+
},
|
|
164
|
+
ctx,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(result.exchangeRate.rate).toBe(0.92);
|
|
168
|
+
expect(result.exchangeRate.sourceCurrencyId).toBe(baseCurrencyUSD.id);
|
|
169
|
+
expect(result.exchangeRate.targetCurrencyId).toBe(baseCurrencyEUR.id);
|
|
170
|
+
expect(spies.insert).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("throws when permission is missing", async () => {
|
|
174
|
+
const { db } = createMockDb<DB>();
|
|
175
|
+
const denied: CommandContext = { actorId: "test-actor", permissions: [] };
|
|
176
|
+
await expect(
|
|
177
|
+
createExchangeRate(
|
|
178
|
+
db,
|
|
179
|
+
{
|
|
180
|
+
sourceCurrencyId: "cur-1",
|
|
181
|
+
targetCurrencyId: "cur-2",
|
|
182
|
+
rate: 1.5,
|
|
183
|
+
effectiveDate: new Date("2024-01-15"),
|
|
184
|
+
},
|
|
185
|
+
denied,
|
|
186
|
+
),
|
|
187
|
+
).rejects.toBeInstanceOf(InsufficientPermissionError);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("passes custom fields through to insert", async () => {
|
|
191
|
+
const createExchangeRateWithFields = makeCreateExchangeRate<{ source: string }>();
|
|
192
|
+
const { db, spies } = createMockDb<DB>();
|
|
193
|
+
const createdRate = {
|
|
194
|
+
...baseExchangeRateUSDtoEUR,
|
|
195
|
+
id: "new-rate-id",
|
|
196
|
+
source: "ECB",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
spies.select.mockReturnValueOnce(baseCurrencyUSD).mockReturnValueOnce(baseCurrencyEUR);
|
|
200
|
+
spies.insert.mockReturnValue(createdRate);
|
|
201
|
+
|
|
202
|
+
await createExchangeRateWithFields(
|
|
203
|
+
db,
|
|
204
|
+
{
|
|
205
|
+
sourceCurrencyId: baseCurrencyUSD.id,
|
|
206
|
+
targetCurrencyId: baseCurrencyEUR.id,
|
|
207
|
+
rate: 0.92,
|
|
208
|
+
effectiveDate: new Date("2024-01-15"),
|
|
209
|
+
source: "ECB",
|
|
210
|
+
},
|
|
211
|
+
ctx,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(spies.values).toHaveBeenNthCalledWith(1, expect.objectContaining({ source: "ECB" }));
|
|
215
|
+
});
|
|
216
|
+
});
|