@tailor-platform/erp-kit 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -28
- package/dist/cli.js +894 -0
- package/package.json +65 -8
- package/rules/app-compose/backend/auth.md +78 -0
- package/rules/app-compose/frontend/auth.md +55 -0
- package/rules/app-compose/frontend/component.md +55 -0
- package/rules/app-compose/frontend/page.md +86 -0
- package/rules/app-compose/frontend/screen-detailview.md +112 -0
- package/rules/app-compose/frontend/screen-form.md +145 -0
- package/rules/app-compose/frontend/screen-listview.md +159 -0
- package/rules/app-compose/structure.md +32 -0
- package/rules/module-development/commands.md +54 -0
- package/rules/module-development/cross-module-type-injection.md +28 -0
- package/rules/module-development/dependency-modules.md +24 -0
- package/rules/module-development/errors.md +12 -0
- package/rules/module-development/executors.md +67 -0
- package/rules/module-development/exports.md +13 -0
- package/rules/module-development/models.md +34 -0
- package/rules/module-development/structure.md +27 -0
- package/rules/module-development/sync-vs-async-operations.md +83 -0
- package/rules/module-development/testing.md +43 -0
- package/rules/sdk-best-practices/db-relations.md +74 -0
- package/rules/sdk-best-practices/sdk-docs.md +14 -0
- 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 +107 -0
- package/skills/2-module-feature-breakdown/SKILL.md +66 -0
- package/skills/3-module-doc-review/SKILL.md +230 -0
- package/skills/4-module-tdd-implementation/SKILL.md +56 -0
- package/skills/5-module-implementation-review/SKILL.md +400 -0
- package/skills/app-compose-1-requirement-analysis/SKILL.md +85 -0
- package/skills/app-compose-2-requirements-breakdown/SKILL.md +88 -0
- package/skills/app-compose-3-doc-review/SKILL.md +112 -0
- package/skills/app-compose-4-design-mock/SKILL.md +248 -0
- package/skills/app-compose-5-design-mock-review/SKILL.md +283 -0
- package/skills/app-compose-6-implementation-spec/SKILL.md +122 -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 +77 -0
- package/src/commands/init.ts +87 -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,62 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import { CannotDeactivateReferenceUnitError, UnitNotFoundError } from "../lib/errors";
|
|
4
|
+
import { permissions } from "../permissions";
|
|
5
|
+
|
|
6
|
+
export interface DeactivateUnitInput {
|
|
7
|
+
unitId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Function: deactivateUnit
|
|
12
|
+
*
|
|
13
|
+
* Disables a unit of measure from being used in new product assignments
|
|
14
|
+
* and quantity conversions while preserving all historical data.
|
|
15
|
+
* Reference units cannot be deactivated.
|
|
16
|
+
*/
|
|
17
|
+
export const deactivateUnit = defineCommand(
|
|
18
|
+
permissions.deactivateUnit,
|
|
19
|
+
async (db: DB, input: DeactivateUnitInput) => {
|
|
20
|
+
// 1. Find unit by ID
|
|
21
|
+
const unit = await db
|
|
22
|
+
.selectFrom("Unit")
|
|
23
|
+
.selectAll()
|
|
24
|
+
.where("id", "=", input.unitId)
|
|
25
|
+
.executeTakeFirst();
|
|
26
|
+
|
|
27
|
+
// 2. If not found, throw error
|
|
28
|
+
if (!unit) {
|
|
29
|
+
throw new UnitNotFoundError(input.unitId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 3. Check if unit is reference unit for its category
|
|
33
|
+
const category = await db
|
|
34
|
+
.selectFrom("UoMCategory")
|
|
35
|
+
.selectAll()
|
|
36
|
+
.where("id", "=", unit.categoryId)
|
|
37
|
+
.executeTakeFirst();
|
|
38
|
+
|
|
39
|
+
if (category?.referenceUnitId === unit.id) {
|
|
40
|
+
throw new CannotDeactivateReferenceUnitError(input.unitId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 4. If already inactive, return unit (idempotent)
|
|
44
|
+
if (!unit.isActive) {
|
|
45
|
+
return { unit };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 5. Update isActive = false
|
|
49
|
+
const updatedUnit = await db
|
|
50
|
+
.updateTable("Unit")
|
|
51
|
+
.set({
|
|
52
|
+
isActive: false,
|
|
53
|
+
updatedAt: new Date(),
|
|
54
|
+
})
|
|
55
|
+
.where("id", "=", input.unitId)
|
|
56
|
+
.returningAll()
|
|
57
|
+
.executeTakeFirst();
|
|
58
|
+
|
|
59
|
+
// 6. Return updated unit
|
|
60
|
+
return { unit: updatedUnit! };
|
|
61
|
+
},
|
|
62
|
+
);
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { CannotSetInactiveAsBaseCurrencyError, CurrencyNotFoundError } from "../lib/errors";
|
|
6
|
+
import { baseCurrencyEUR, baseCurrencyUSD, inactiveCurrency } from "../testing/fixtures";
|
|
7
|
+
import { setBaseCurrency } from "./setBaseCurrency";
|
|
8
|
+
|
|
9
|
+
describe("setBaseCurrency", () => {
|
|
10
|
+
const ctx: CommandContext = {
|
|
11
|
+
actorId: "test-actor",
|
|
12
|
+
permissions: ["primitives:setBaseCurrency"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Error cases
|
|
16
|
+
it("throws when currency doesn't exist", async () => {
|
|
17
|
+
const { db, spies } = createMockDb<DB>();
|
|
18
|
+
spies.select.mockReturnValue(undefined);
|
|
19
|
+
|
|
20
|
+
await expect(
|
|
21
|
+
setBaseCurrency(
|
|
22
|
+
db,
|
|
23
|
+
{
|
|
24
|
+
currencyId: "nonexistent-currency",
|
|
25
|
+
},
|
|
26
|
+
ctx,
|
|
27
|
+
),
|
|
28
|
+
).rejects.toBeInstanceOf(CurrencyNotFoundError);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("throws when currency is inactive", async () => {
|
|
32
|
+
const { db, spies } = createMockDb<DB>();
|
|
33
|
+
spies.select.mockReturnValue(inactiveCurrency);
|
|
34
|
+
|
|
35
|
+
await expect(
|
|
36
|
+
setBaseCurrency(
|
|
37
|
+
db,
|
|
38
|
+
{
|
|
39
|
+
currencyId: inactiveCurrency.id,
|
|
40
|
+
},
|
|
41
|
+
ctx,
|
|
42
|
+
),
|
|
43
|
+
).rejects.toBeInstanceOf(CannotSetInactiveAsBaseCurrencyError);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Idempotent cases
|
|
47
|
+
it("returns currency unchanged when already base", async () => {
|
|
48
|
+
const { db, spies } = createMockDb<DB>();
|
|
49
|
+
spies.select.mockReturnValue(baseCurrencyUSD); // Already base currency
|
|
50
|
+
|
|
51
|
+
const result = await setBaseCurrency(
|
|
52
|
+
db,
|
|
53
|
+
{
|
|
54
|
+
currencyId: baseCurrencyUSD.id,
|
|
55
|
+
},
|
|
56
|
+
ctx,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(result.currency).toEqual(baseCurrencyUSD);
|
|
60
|
+
expect(spies.update).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Success cases
|
|
64
|
+
it("changes base currency", async () => {
|
|
65
|
+
const { db, spies } = createMockDb<DB>();
|
|
66
|
+
const newBaseCurrency = {
|
|
67
|
+
...baseCurrencyEUR,
|
|
68
|
+
isBaseCurrency: true,
|
|
69
|
+
updatedAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
spies.select
|
|
73
|
+
.mockReturnValueOnce(baseCurrencyEUR) // Target currency lookup
|
|
74
|
+
.mockReturnValueOnce(baseCurrencyUSD); // Current base currency lookup
|
|
75
|
+
spies.update
|
|
76
|
+
.mockReturnValueOnce({ ...baseCurrencyUSD, isBaseCurrency: false }) // Remove base from old
|
|
77
|
+
.mockReturnValueOnce(newBaseCurrency); // Set base on new
|
|
78
|
+
|
|
79
|
+
const result = await setBaseCurrency(
|
|
80
|
+
db,
|
|
81
|
+
{
|
|
82
|
+
currencyId: baseCurrencyEUR.id,
|
|
83
|
+
},
|
|
84
|
+
ctx,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result.currency.isBaseCurrency).toBe(true);
|
|
88
|
+
expect(spies.update).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("throws when permission is missing", async () => {
|
|
92
|
+
const { db } = createMockDb<DB>();
|
|
93
|
+
const denied: CommandContext = { actorId: "test-actor", permissions: [] };
|
|
94
|
+
await expect(setBaseCurrency(db, { currencyId: "cur-1" }, denied)).rejects.toBeInstanceOf(
|
|
95
|
+
InsufficientPermissionError,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import { CannotSetInactiveAsBaseCurrencyError, CurrencyNotFoundError } from "../lib/errors";
|
|
4
|
+
import { permissions } from "../permissions";
|
|
5
|
+
|
|
6
|
+
export interface SetBaseCurrencyInput {
|
|
7
|
+
currencyId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Function: setBaseCurrency
|
|
12
|
+
*
|
|
13
|
+
* Changes the organization's base currency to a different active currency.
|
|
14
|
+
* The base currency serves as the default for financial reporting.
|
|
15
|
+
*/
|
|
16
|
+
export const setBaseCurrency = defineCommand(
|
|
17
|
+
permissions.setBaseCurrency,
|
|
18
|
+
async (db: DB, input: SetBaseCurrencyInput) => {
|
|
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 currency is active
|
|
32
|
+
if (!currency.isActive) {
|
|
33
|
+
throw new CannotSetInactiveAsBaseCurrencyError(input.currencyId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 4. If already base currency, return (idempotent)
|
|
37
|
+
if (currency.isBaseCurrency) {
|
|
38
|
+
return { currency };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 5. Find current base currency
|
|
42
|
+
const currentBase = await db
|
|
43
|
+
.selectFrom("Currency")
|
|
44
|
+
.selectAll()
|
|
45
|
+
.where("isBaseCurrency", "=", true)
|
|
46
|
+
.executeTakeFirst();
|
|
47
|
+
|
|
48
|
+
// 6. Remove base flag from current base
|
|
49
|
+
if (currentBase) {
|
|
50
|
+
await db
|
|
51
|
+
.updateTable("Currency")
|
|
52
|
+
.set({
|
|
53
|
+
isBaseCurrency: false,
|
|
54
|
+
updatedAt: new Date(),
|
|
55
|
+
})
|
|
56
|
+
.where("id", "=", currentBase.id)
|
|
57
|
+
.returningAll()
|
|
58
|
+
.executeTakeFirst();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 7. Set base flag on target currency
|
|
62
|
+
const updatedCurrency = await db
|
|
63
|
+
.updateTable("Currency")
|
|
64
|
+
.set({
|
|
65
|
+
isBaseCurrency: true,
|
|
66
|
+
updatedAt: new Date(),
|
|
67
|
+
})
|
|
68
|
+
.where("id", "=", input.currencyId)
|
|
69
|
+
.returningAll()
|
|
70
|
+
.executeTakeFirst();
|
|
71
|
+
|
|
72
|
+
return { currency: updatedCurrency! };
|
|
73
|
+
},
|
|
74
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
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 { UnitNotFoundError, UnitNotInCategoryError } from "../lib/errors";
|
|
6
|
+
import { baseUoMCategory, baseUnitGram, baseUnitKg, baseUnitLiter } from "../testing/fixtures";
|
|
7
|
+
import { setReferenceUnit } from "./setReferenceUnit";
|
|
8
|
+
|
|
9
|
+
describe("setReferenceUnit", () => {
|
|
10
|
+
const ctx: CommandContext = {
|
|
11
|
+
actorId: "test-actor",
|
|
12
|
+
permissions: ["primitives:setReferenceUnit"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Error cases
|
|
16
|
+
it("throws when unit doesn't exist", async () => {
|
|
17
|
+
const { db, spies } = createMockDb<DB>();
|
|
18
|
+
spies.select.mockReturnValue(undefined);
|
|
19
|
+
|
|
20
|
+
await expect(
|
|
21
|
+
setReferenceUnit(
|
|
22
|
+
db,
|
|
23
|
+
{
|
|
24
|
+
unitId: "nonexistent-unit",
|
|
25
|
+
categoryId: baseUoMCategory.id,
|
|
26
|
+
},
|
|
27
|
+
ctx,
|
|
28
|
+
),
|
|
29
|
+
).rejects.toBeInstanceOf(UnitNotFoundError);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("throws when unit is not in specified category", async () => {
|
|
33
|
+
const { db, spies } = createMockDb<DB>();
|
|
34
|
+
spies.select.mockReturnValue(baseUnitLiter); // Unit is in category-2, not category-1
|
|
35
|
+
|
|
36
|
+
await expect(
|
|
37
|
+
setReferenceUnit(
|
|
38
|
+
db,
|
|
39
|
+
{
|
|
40
|
+
unitId: baseUnitLiter.id,
|
|
41
|
+
categoryId: baseUoMCategory.id, // category-1
|
|
42
|
+
},
|
|
43
|
+
ctx,
|
|
44
|
+
),
|
|
45
|
+
).rejects.toBeInstanceOf(UnitNotInCategoryError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Idempotent cases
|
|
49
|
+
it("returns category unchanged when unit is already reference", async () => {
|
|
50
|
+
const { db, spies } = createMockDb<DB>();
|
|
51
|
+
spies.select
|
|
52
|
+
.mockReturnValueOnce(baseUnitKg) // Unit lookup
|
|
53
|
+
.mockReturnValueOnce(baseUoMCategory); // Category lookup
|
|
54
|
+
|
|
55
|
+
const result = await setReferenceUnit(
|
|
56
|
+
db,
|
|
57
|
+
{
|
|
58
|
+
unitId: baseUnitKg.id,
|
|
59
|
+
categoryId: baseUoMCategory.id,
|
|
60
|
+
},
|
|
61
|
+
ctx,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(result.category.referenceUnitId).toBe(baseUnitKg.id);
|
|
65
|
+
expect(spies.update).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Success cases
|
|
69
|
+
it("changes reference unit and recalculates conversion factors", async () => {
|
|
70
|
+
const { db, spies } = createMockDb<DB>();
|
|
71
|
+
// gram has conversion factor 0.001 (1g = 0.001kg)
|
|
72
|
+
// When gram becomes reference, kg should become 1000 (1kg = 1000g)
|
|
73
|
+
const updatedCategory = {
|
|
74
|
+
...baseUoMCategory,
|
|
75
|
+
referenceUnitId: baseUnitGram.id,
|
|
76
|
+
updatedAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
spies.select
|
|
80
|
+
.mockReturnValueOnce(baseUnitGram) // Unit lookup
|
|
81
|
+
.mockReturnValueOnce(baseUoMCategory) // Category lookup
|
|
82
|
+
.mockReturnValueOnce([baseUnitKg, baseUnitGram]); // All units in category
|
|
83
|
+
spies.update
|
|
84
|
+
.mockReturnValueOnce({ ...baseUnitKg, conversionFactor: 1000 }) // Update kg factor
|
|
85
|
+
.mockReturnValueOnce({ ...baseUnitGram, conversionFactor: 1.0 }) // Update gram factor
|
|
86
|
+
.mockReturnValueOnce(updatedCategory); // Update category
|
|
87
|
+
|
|
88
|
+
const result = await setReferenceUnit(
|
|
89
|
+
db,
|
|
90
|
+
{
|
|
91
|
+
unitId: baseUnitGram.id,
|
|
92
|
+
categoryId: baseUoMCategory.id,
|
|
93
|
+
},
|
|
94
|
+
ctx,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(result.category.referenceUnitId).toBe(baseUnitGram.id);
|
|
98
|
+
expect(spies.update).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("throws when permission is missing", async () => {
|
|
102
|
+
const { db } = createMockDb<DB>();
|
|
103
|
+
const denied: CommandContext = { actorId: "test-actor", permissions: [] };
|
|
104
|
+
await expect(
|
|
105
|
+
setReferenceUnit(db, { unitId: "unit-1", categoryId: "cat-1" }, denied),
|
|
106
|
+
).rejects.toBeInstanceOf(InsufficientPermissionError);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineCommand } from "../../shared/internal";
|
|
2
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
3
|
+
import { UnitNotFoundError, UnitNotInCategoryError } from "../lib/errors";
|
|
4
|
+
import { permissions } from "../permissions";
|
|
5
|
+
|
|
6
|
+
export interface SetReferenceUnitInput {
|
|
7
|
+
unitId: string;
|
|
8
|
+
categoryId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Function: setReferenceUnit
|
|
13
|
+
*
|
|
14
|
+
* Changes the reference unit for a UoM category. All conversion factors
|
|
15
|
+
* are recalculated relative to the new reference unit.
|
|
16
|
+
*/
|
|
17
|
+
export const setReferenceUnit = defineCommand(
|
|
18
|
+
permissions.setReferenceUnit,
|
|
19
|
+
async (db: DB, input: SetReferenceUnitInput) => {
|
|
20
|
+
// 1. Find unit by ID
|
|
21
|
+
const unit = await db
|
|
22
|
+
.selectFrom("Unit")
|
|
23
|
+
.selectAll()
|
|
24
|
+
.where("id", "=", input.unitId)
|
|
25
|
+
.executeTakeFirst();
|
|
26
|
+
|
|
27
|
+
if (!unit) {
|
|
28
|
+
throw new UnitNotFoundError(input.unitId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Check unit belongs to specified category
|
|
32
|
+
if (unit.categoryId !== input.categoryId) {
|
|
33
|
+
throw new UnitNotInCategoryError(input.unitId, input.categoryId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Get category
|
|
37
|
+
const category = await db
|
|
38
|
+
.selectFrom("UoMCategory")
|
|
39
|
+
.selectAll()
|
|
40
|
+
.where("id", "=", input.categoryId)
|
|
41
|
+
.executeTakeFirst();
|
|
42
|
+
|
|
43
|
+
// 4. If already reference unit, return (idempotent)
|
|
44
|
+
if (category?.referenceUnitId === input.unitId && category) {
|
|
45
|
+
return { category };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 5. Get all units in category for recalculation
|
|
49
|
+
const units = await db
|
|
50
|
+
.selectFrom("Unit")
|
|
51
|
+
.selectAll()
|
|
52
|
+
.where("categoryId", "=", input.categoryId)
|
|
53
|
+
.execute();
|
|
54
|
+
|
|
55
|
+
// 6. Recalculate all conversion factors
|
|
56
|
+
// new_factor = old_factor / new_reference_old_factor
|
|
57
|
+
const newReferenceFactor = unit.conversionFactor;
|
|
58
|
+
for (const u of units) {
|
|
59
|
+
const newFactor = u.conversionFactor / newReferenceFactor;
|
|
60
|
+
await db
|
|
61
|
+
.updateTable("Unit")
|
|
62
|
+
.set({
|
|
63
|
+
conversionFactor: newFactor,
|
|
64
|
+
updatedAt: new Date(),
|
|
65
|
+
})
|
|
66
|
+
.where("id", "=", u.id)
|
|
67
|
+
.returningAll()
|
|
68
|
+
.executeTakeFirst();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 7. Update category reference unit
|
|
72
|
+
const updatedCategory = await db
|
|
73
|
+
.updateTable("UoMCategory")
|
|
74
|
+
.set({
|
|
75
|
+
referenceUnitId: input.unitId,
|
|
76
|
+
updatedAt: new Date(),
|
|
77
|
+
})
|
|
78
|
+
.where("id", "=", input.categoryId)
|
|
79
|
+
.returningAll()
|
|
80
|
+
.executeTakeFirst();
|
|
81
|
+
|
|
82
|
+
return { category: updatedCategory! };
|
|
83
|
+
},
|
|
84
|
+
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
type TailorAnyDBField,
|
|
4
|
+
unsafeAllowAllGqlPermission,
|
|
5
|
+
unsafeAllowAllTypePermission,
|
|
6
|
+
} from "@tailor-platform/sdk";
|
|
7
|
+
|
|
8
|
+
export interface CreateCurrencyTypeParams<F extends Record<string, TailorAnyDBField>> {
|
|
9
|
+
fields?: F;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createCurrencyType<const F extends Record<string, TailorAnyDBField>>(
|
|
13
|
+
params: CreateCurrencyTypeParams<F>,
|
|
14
|
+
) {
|
|
15
|
+
return db
|
|
16
|
+
.type("Currency", {
|
|
17
|
+
code: db.string().unique().description("ISO 4217 currency code (e.g., USD)"),
|
|
18
|
+
name: db.string().description("Currency display name (e.g., US Dollar)"),
|
|
19
|
+
symbol: db.string().description("Currency symbol (e.g., $)"),
|
|
20
|
+
decimalPlaces: db.int().description("Number of decimal places (0-4)"),
|
|
21
|
+
isBaseCurrency: db.bool().description("Whether this is the base currency"),
|
|
22
|
+
isActive: db.bool().description("Whether the currency is active"),
|
|
23
|
+
...((params.fields ?? {}) as F),
|
|
24
|
+
...db.fields.timestamps(),
|
|
25
|
+
})
|
|
26
|
+
.permission(unsafeAllowAllTypePermission)
|
|
27
|
+
.gqlPermission(unsafeAllowAllGqlPermission);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const currency = createCurrencyType({});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
type TailorAnyDBField,
|
|
4
|
+
unsafeAllowAllGqlPermission,
|
|
5
|
+
unsafeAllowAllTypePermission,
|
|
6
|
+
} from "@tailor-platform/sdk";
|
|
7
|
+
|
|
8
|
+
export interface CreateExchangeRateTypeParams<F extends Record<string, TailorAnyDBField>> {
|
|
9
|
+
fields?: F;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createExchangeRateType<const F extends Record<string, TailorAnyDBField>>(
|
|
13
|
+
params: CreateExchangeRateTypeParams<F>,
|
|
14
|
+
) {
|
|
15
|
+
return db
|
|
16
|
+
.type("ExchangeRate", {
|
|
17
|
+
sourceCurrencyId: db.uuid().description("Foreign key to source Currency"),
|
|
18
|
+
targetCurrencyId: db.uuid().description("Foreign key to target Currency"),
|
|
19
|
+
rate: db.float().description("Conversion rate (source x rate = target)"),
|
|
20
|
+
effectiveDate: db.date().description("Date from which this rate applies"),
|
|
21
|
+
...((params.fields ?? {}) as F),
|
|
22
|
+
...db.fields.timestamps(),
|
|
23
|
+
})
|
|
24
|
+
.permission(unsafeAllowAllTypePermission)
|
|
25
|
+
.gqlPermission(unsafeAllowAllGqlPermission);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const exchangeRate = createExchangeRateType({});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
type TailorAnyDBField,
|
|
4
|
+
unsafeAllowAllGqlPermission,
|
|
5
|
+
unsafeAllowAllTypePermission,
|
|
6
|
+
} from "@tailor-platform/sdk";
|
|
7
|
+
|
|
8
|
+
export interface CreateUnitTypeParams<F extends Record<string, TailorAnyDBField>> {
|
|
9
|
+
fields?: F;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createUnitType<const F extends Record<string, TailorAnyDBField>>(
|
|
13
|
+
params: CreateUnitTypeParams<F>,
|
|
14
|
+
) {
|
|
15
|
+
return db
|
|
16
|
+
.type("Unit", {
|
|
17
|
+
name: db.string().description("Unit display name (e.g., Kilogram)"),
|
|
18
|
+
symbol: db.string().description("Short symbol (e.g., kg)"),
|
|
19
|
+
categoryId: db.uuid().description("Foreign key to UoMCategory"),
|
|
20
|
+
conversionFactor: db
|
|
21
|
+
.float()
|
|
22
|
+
.description("Multiplier relative to reference unit (reference unit = 1.0)"),
|
|
23
|
+
roundingPrecision: db.int().description("Decimal places for rounding"),
|
|
24
|
+
isActive: db.bool().description("Whether the unit is active"),
|
|
25
|
+
...((params.fields ?? {}) as F),
|
|
26
|
+
...db.fields.timestamps(),
|
|
27
|
+
})
|
|
28
|
+
.permission(unsafeAllowAllTypePermission)
|
|
29
|
+
.gqlPermission(unsafeAllowAllGqlPermission);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const unit = createUnitType({});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
type TailorAnyDBField,
|
|
4
|
+
unsafeAllowAllGqlPermission,
|
|
5
|
+
unsafeAllowAllTypePermission,
|
|
6
|
+
} from "@tailor-platform/sdk";
|
|
7
|
+
|
|
8
|
+
export interface CreateUoMCategoryTypeParams<F extends Record<string, TailorAnyDBField>> {
|
|
9
|
+
fields?: F;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createUoMCategoryType<const F extends Record<string, TailorAnyDBField>>(
|
|
13
|
+
params: CreateUoMCategoryTypeParams<F>,
|
|
14
|
+
) {
|
|
15
|
+
return db
|
|
16
|
+
.type("UoMCategory", {
|
|
17
|
+
name: db.string().unique().description("Category name (e.g., Weight, Volume, Length)"),
|
|
18
|
+
description: db
|
|
19
|
+
.string({ optional: true })
|
|
20
|
+
.description("Optional description of the category"),
|
|
21
|
+
referenceUnitId: db
|
|
22
|
+
.uuid({ optional: true })
|
|
23
|
+
.description("Foreign key to the reference Unit for conversions"),
|
|
24
|
+
isActive: db.bool().description("Whether the category is active"),
|
|
25
|
+
...((params.fields ?? {}) as F),
|
|
26
|
+
...db.fields.timestamps(),
|
|
27
|
+
})
|
|
28
|
+
.permission(unsafeAllowAllTypePermission)
|
|
29
|
+
.gqlPermission(unsafeAllowAllGqlPermission);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const uomCategory = createUoMCategoryType({});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ActivateCategory
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
ActivateCategory re-enables a previously deactivated UoM category, making it and its units available for new product assignments and transactions. This command supports scenarios where a temporarily suspended category needs to be restored to active use.
|
|
6
|
+
|
|
7
|
+
## Business Rules
|
|
8
|
+
|
|
9
|
+
- Target category must exist in the system
|
|
10
|
+
- Target category must be in Inactive status
|
|
11
|
+
- Activating an already active category returns success with no state change
|
|
12
|
+
- All units within the category remain in their current state (active or inactive)
|
|
13
|
+
- Products previously using this category's units are not automatically re-enabled
|
|
14
|
+
|
|
15
|
+
## Process Flow
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart TD
|
|
19
|
+
A[Receive activate request] --> B{Category exists?}
|
|
20
|
+
B -->|No| C[Return error: not found]
|
|
21
|
+
B -->|Yes| D{Current status?}
|
|
22
|
+
D -->|Active| E[Return success: already active]
|
|
23
|
+
D -->|Inactive| F[Update status to Active]
|
|
24
|
+
F --> G[Return activated category]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## External Dependencies
|
|
28
|
+
|
|
29
|
+
- None
|
|
30
|
+
|
|
31
|
+
## Error Scenarios
|
|
32
|
+
|
|
33
|
+
- **Category not found**: Specified category ID does not exist - return not found error
|
|
34
|
+
- **Invalid category ID**: Malformed or empty ID provided - return validation error
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ActivateCurrency
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
ActivateCurrency re-enables a previously deactivated currency, making it available for new transactions. This command supports scenarios where a temporarily suspended currency needs to be restored to active use, such as resuming operations in a market after a temporary exit.
|
|
6
|
+
|
|
7
|
+
## Business Rules
|
|
8
|
+
|
|
9
|
+
- Target currency must exist in the system
|
|
10
|
+
- Target currency must be in Inactive status
|
|
11
|
+
- Activating an already active currency returns success with no state change
|
|
12
|
+
- No impact on historical transactions that used this currency while inactive
|
|
13
|
+
|
|
14
|
+
## Process Flow
|
|
15
|
+
|
|
16
|
+
```mermaid
|
|
17
|
+
flowchart TD
|
|
18
|
+
A[Receive activate request] --> B{Currency exists?}
|
|
19
|
+
B -->|No| C[Return error: not found]
|
|
20
|
+
B -->|Yes| D{Current status?}
|
|
21
|
+
D -->|Active| E[Return success: already active]
|
|
22
|
+
D -->|Inactive| F[Update status to Active]
|
|
23
|
+
F --> G[Return activated currency]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## External Dependencies
|
|
27
|
+
|
|
28
|
+
- None
|
|
29
|
+
|
|
30
|
+
## Error Scenarios
|
|
31
|
+
|
|
32
|
+
- **Currency not found**: Specified currency ID does not exist - return not found error
|
|
33
|
+
- **Invalid currency ID**: Malformed or empty ID provided - return validation error
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ActivateUnit
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
ActivateUnit re-enables a previously deactivated unit of measure, making it available for new product assignments and quantity conversions. This command supports scenarios where a temporarily suspended unit needs to be restored to active use.
|
|
6
|
+
|
|
7
|
+
## Business Rules
|
|
8
|
+
|
|
9
|
+
- Target unit must exist in the system
|
|
10
|
+
- Target unit must be in Inactive status
|
|
11
|
+
- Activating an already active unit returns success with no state change
|
|
12
|
+
- The unit's category must be active for the unit to be usable in new assignments
|
|
13
|
+
- Historical transactions using this unit are not affected by activation
|
|
14
|
+
|
|
15
|
+
## Process Flow
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart TD
|
|
19
|
+
A[Receive activate request] --> B{Unit exists?}
|
|
20
|
+
B -->|No| C[Return error: not found]
|
|
21
|
+
B -->|Yes| D{Current status?}
|
|
22
|
+
D -->|Active| E[Return success: already active]
|
|
23
|
+
D -->|Inactive| F[Update status to Active]
|
|
24
|
+
F --> G[Return activated unit]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## External Dependencies
|
|
28
|
+
|
|
29
|
+
- None
|
|
30
|
+
|
|
31
|
+
## Error Scenarios
|
|
32
|
+
|
|
33
|
+
- **Unit not found**: Specified unit ID does not exist - return not found error
|
|
34
|
+
- **Invalid unit ID**: Malformed or empty ID provided - return validation error
|