@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.
Files changed (213) hide show
  1. package/README.md +196 -28
  2. package/dist/cli.js +894 -0
  3. package/package.json +65 -8
  4. package/rules/app-compose/backend/auth.md +78 -0
  5. package/rules/app-compose/frontend/auth.md +55 -0
  6. package/rules/app-compose/frontend/component.md +55 -0
  7. package/rules/app-compose/frontend/page.md +86 -0
  8. package/rules/app-compose/frontend/screen-detailview.md +112 -0
  9. package/rules/app-compose/frontend/screen-form.md +145 -0
  10. package/rules/app-compose/frontend/screen-listview.md +159 -0
  11. package/rules/app-compose/structure.md +32 -0
  12. package/rules/module-development/commands.md +54 -0
  13. package/rules/module-development/cross-module-type-injection.md +28 -0
  14. package/rules/module-development/dependency-modules.md +24 -0
  15. package/rules/module-development/errors.md +12 -0
  16. package/rules/module-development/executors.md +67 -0
  17. package/rules/module-development/exports.md +13 -0
  18. package/rules/module-development/models.md +34 -0
  19. package/rules/module-development/structure.md +27 -0
  20. package/rules/module-development/sync-vs-async-operations.md +83 -0
  21. package/rules/module-development/testing.md +43 -0
  22. package/rules/sdk-best-practices/db-relations.md +74 -0
  23. package/rules/sdk-best-practices/sdk-docs.md +14 -0
  24. package/schemas/app-compose/actors.yml +34 -0
  25. package/schemas/app-compose/business-flow.yml +50 -0
  26. package/schemas/app-compose/requirements.yml +33 -0
  27. package/schemas/app-compose/resolver.yml +47 -0
  28. package/schemas/app-compose/screen.yml +81 -0
  29. package/schemas/app-compose/story.yml +67 -0
  30. package/schemas/module/command.yml +52 -0
  31. package/schemas/module/feature.yml +58 -0
  32. package/schemas/module/model.yml +70 -0
  33. package/schemas/module/module.yml +50 -0
  34. package/skills/1-module-docs/SKILL.md +107 -0
  35. package/skills/2-module-feature-breakdown/SKILL.md +66 -0
  36. package/skills/3-module-doc-review/SKILL.md +230 -0
  37. package/skills/4-module-tdd-implementation/SKILL.md +56 -0
  38. package/skills/5-module-implementation-review/SKILL.md +400 -0
  39. package/skills/app-compose-1-requirement-analysis/SKILL.md +85 -0
  40. package/skills/app-compose-2-requirements-breakdown/SKILL.md +88 -0
  41. package/skills/app-compose-3-doc-review/SKILL.md +112 -0
  42. package/skills/app-compose-4-design-mock/SKILL.md +248 -0
  43. package/skills/app-compose-5-design-mock-review/SKILL.md +283 -0
  44. package/skills/app-compose-6-implementation-spec/SKILL.md +122 -0
  45. package/skills/mock-scenario/SKILL.md +118 -0
  46. package/src/app.ts +1 -0
  47. package/src/cli.ts +120 -0
  48. package/src/commands/check.test.ts +30 -0
  49. package/src/commands/check.ts +66 -0
  50. package/src/commands/init.test.ts +77 -0
  51. package/src/commands/init.ts +87 -0
  52. package/src/commands/mock/index.ts +53 -0
  53. package/src/commands/mock/start.ts +179 -0
  54. package/src/commands/mock/validate.test.ts +185 -0
  55. package/src/commands/mock/validate.ts +198 -0
  56. package/src/commands/scaffold.test.ts +76 -0
  57. package/src/commands/scaffold.ts +119 -0
  58. package/src/commands/sync-check.test.ts +125 -0
  59. package/src/commands/sync-check.ts +182 -0
  60. package/src/integration.test.ts +63 -0
  61. package/src/mdschema.ts +48 -0
  62. package/src/mockServer.ts +55 -0
  63. package/src/module.ts +86 -0
  64. package/src/modules/accounting/.gitkeep +0 -0
  65. package/src/modules/coa-management/.gitkeep +0 -0
  66. package/src/modules/inventory/.gitkeep +0 -0
  67. package/src/modules/manufacturing/.gitkeep +0 -0
  68. package/src/modules/primitives/README.md +39 -0
  69. package/src/modules/primitives/command/activateCategory.test.ts +75 -0
  70. package/src/modules/primitives/command/activateCategory.ts +50 -0
  71. package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
  72. package/src/modules/primitives/command/activateCurrency.ts +50 -0
  73. package/src/modules/primitives/command/activateUnit.test.ts +53 -0
  74. package/src/modules/primitives/command/activateUnit.ts +50 -0
  75. package/src/modules/primitives/command/convertAmount.test.ts +275 -0
  76. package/src/modules/primitives/command/convertAmount.ts +126 -0
  77. package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
  78. package/src/modules/primitives/command/convertQuantity.ts +73 -0
  79. package/src/modules/primitives/command/createCategory.test.ts +126 -0
  80. package/src/modules/primitives/command/createCategory.ts +89 -0
  81. package/src/modules/primitives/command/createCurrency.test.ts +191 -0
  82. package/src/modules/primitives/command/createCurrency.ts +77 -0
  83. package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
  84. package/src/modules/primitives/command/createExchangeRate.ts +91 -0
  85. package/src/modules/primitives/command/createUnit.test.ts +214 -0
  86. package/src/modules/primitives/command/createUnit.ts +88 -0
  87. package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
  88. package/src/modules/primitives/command/deactivateCategory.ts +62 -0
  89. package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
  90. package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
  91. package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
  92. package/src/modules/primitives/command/deactivateUnit.ts +62 -0
  93. package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
  94. package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
  95. package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
  96. package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
  97. package/src/modules/primitives/db/currency.ts +30 -0
  98. package/src/modules/primitives/db/exchangeRate.ts +28 -0
  99. package/src/modules/primitives/db/unit.ts +32 -0
  100. package/src/modules/primitives/db/uomCategory.ts +32 -0
  101. package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
  102. package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
  103. package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
  104. package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
  105. package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
  106. package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
  107. package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
  108. package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
  109. package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
  110. package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
  111. package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
  112. package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
  113. package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
  114. package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
  115. package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
  116. package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
  117. package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
  118. package/src/modules/primitives/docs/features/uom-categories.md +52 -0
  119. package/src/modules/primitives/docs/models/Currency.md +45 -0
  120. package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
  121. package/src/modules/primitives/docs/models/Unit.md +46 -0
  122. package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
  123. package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
  124. package/src/modules/primitives/index.ts +40 -0
  125. package/src/modules/primitives/lib/errors.ts +138 -0
  126. package/src/modules/primitives/lib/types.ts +20 -0
  127. package/src/modules/primitives/module.ts +66 -0
  128. package/src/modules/primitives/permissions.ts +18 -0
  129. package/src/modules/primitives/tailor.config.ts +11 -0
  130. package/src/modules/primitives/testing/fixtures.ts +161 -0
  131. package/src/modules/product-management/.gitkeep +0 -0
  132. package/src/modules/purchase/.gitkeep +0 -0
  133. package/src/modules/sales/.gitkeep +0 -0
  134. package/src/modules/shared/createContext.test.ts +39 -0
  135. package/src/modules/shared/createContext.ts +15 -0
  136. package/src/modules/shared/defineCommand.test.ts +42 -0
  137. package/src/modules/shared/defineCommand.ts +19 -0
  138. package/src/modules/shared/definePermissions.test.ts +146 -0
  139. package/src/modules/shared/definePermissions.ts +94 -0
  140. package/src/modules/shared/entityTypes.ts +15 -0
  141. package/src/modules/shared/errors.ts +22 -0
  142. package/src/modules/shared/index.ts +1 -0
  143. package/src/modules/shared/internal.ts +13 -0
  144. package/src/modules/shared/requirePermission.test.ts +47 -0
  145. package/src/modules/shared/requirePermission.ts +8 -0
  146. package/src/modules/shared/types.ts +4 -0
  147. package/src/modules/supplier-management/.gitkeep +0 -0
  148. package/src/modules/supplier-portal/.gitkeep +0 -0
  149. package/src/modules/testing/index.ts +120 -0
  150. package/src/modules/user-management/README.md +38 -0
  151. package/src/modules/user-management/command/activateUser.test.ts +112 -0
  152. package/src/modules/user-management/command/activateUser.ts +67 -0
  153. package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
  154. package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
  155. package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
  156. package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
  157. package/src/modules/user-management/command/createPermission.test.ts +143 -0
  158. package/src/modules/user-management/command/createPermission.ts +66 -0
  159. package/src/modules/user-management/command/createRole.test.ts +115 -0
  160. package/src/modules/user-management/command/createRole.ts +52 -0
  161. package/src/modules/user-management/command/createUser.test.ts +198 -0
  162. package/src/modules/user-management/command/createUser.ts +85 -0
  163. package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
  164. package/src/modules/user-management/command/deactivateUser.ts +67 -0
  165. package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
  166. package/src/modules/user-management/command/logAuditEvent.ts +59 -0
  167. package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
  168. package/src/modules/user-management/command/reactivateUser.ts +67 -0
  169. package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
  170. package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
  171. package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
  172. package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
  173. package/src/modules/user-management/db/auditEvent.ts +47 -0
  174. package/src/modules/user-management/db/permission.ts +31 -0
  175. package/src/modules/user-management/db/role.ts +28 -0
  176. package/src/modules/user-management/db/rolePermission.ts +44 -0
  177. package/src/modules/user-management/db/user.ts +38 -0
  178. package/src/modules/user-management/db/userRole.ts +44 -0
  179. package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
  180. package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
  181. package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
  182. package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
  183. package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
  184. package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
  185. package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
  186. package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
  187. package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
  188. package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
  189. package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
  190. package/src/modules/user-management/docs/features/audit-trail.md +80 -0
  191. package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
  192. package/src/modules/user-management/docs/features/user-account-management.md +64 -0
  193. package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
  194. package/src/modules/user-management/docs/models/Permission.md +31 -0
  195. package/src/modules/user-management/docs/models/Role.md +31 -0
  196. package/src/modules/user-management/docs/models/RolePermission.md +33 -0
  197. package/src/modules/user-management/docs/models/User.md +47 -0
  198. package/src/modules/user-management/docs/models/UserRole.md +34 -0
  199. package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
  200. package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
  201. package/src/modules/user-management/generated/enums.ts +24 -0
  202. package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
  203. package/src/modules/user-management/index.ts +32 -0
  204. package/src/modules/user-management/lib/errors.ts +81 -0
  205. package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
  206. package/src/modules/user-management/lib/types.ts +31 -0
  207. package/src/modules/user-management/module.ts +77 -0
  208. package/src/modules/user-management/permissions.ts +15 -0
  209. package/src/modules/user-management/tailor.config.ts +11 -0
  210. package/src/modules/user-management/testing/fixtures.ts +98 -0
  211. package/src/schemas.ts +25 -0
  212. package/src/testing.ts +10 -0
  213. 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