@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,78 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMockDb, testNotFound, testPermissionDenied } from "../../testing/index";
|
|
3
|
+
import { type CommandContext } from "../../shared/internal";
|
|
4
|
+
import { DB } from "../generated/kysely-tailordb";
|
|
5
|
+
import { CannotDeactivateReferenceUnitError, UnitNotFoundError } from "../lib/errors";
|
|
6
|
+
import { baseUnitKg, baseUnitGram, baseUoMCategory, inactiveUnit } from "../testing/fixtures";
|
|
7
|
+
import { deactivateUnit } from "./deactivateUnit";
|
|
8
|
+
|
|
9
|
+
describe("deactivateUnit", () => {
|
|
10
|
+
const ctx: CommandContext = { actorId: "test-actor", permissions: ["primitives:deactivateUnit"] };
|
|
11
|
+
|
|
12
|
+
it(
|
|
13
|
+
"throws when unit doesn't exist",
|
|
14
|
+
testNotFound(deactivateUnit, { unitId: "nonexistent-unit" }, ctx, UnitNotFoundError),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
it("throws when attempting to deactivate reference unit", async () => {
|
|
18
|
+
const { db, spies } = createMockDb<DB>();
|
|
19
|
+
spies.select.mockReturnValueOnce(baseUnitKg).mockReturnValueOnce(baseUoMCategory);
|
|
20
|
+
|
|
21
|
+
await expect(
|
|
22
|
+
deactivateUnit(
|
|
23
|
+
db,
|
|
24
|
+
{
|
|
25
|
+
unitId: baseUnitKg.id,
|
|
26
|
+
},
|
|
27
|
+
ctx,
|
|
28
|
+
),
|
|
29
|
+
).rejects.toBeInstanceOf(CannotDeactivateReferenceUnitError);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Idempotent case needs multi-mock setup, kept inline
|
|
33
|
+
it("returns unit unchanged when already inactive", async () => {
|
|
34
|
+
const { db, spies } = createMockDb<DB>();
|
|
35
|
+
spies.select.mockReturnValueOnce(inactiveUnit).mockReturnValueOnce(baseUoMCategory);
|
|
36
|
+
|
|
37
|
+
const result = await deactivateUnit(
|
|
38
|
+
db,
|
|
39
|
+
{
|
|
40
|
+
unitId: inactiveUnit.id,
|
|
41
|
+
},
|
|
42
|
+
ctx,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(result.unit).toEqual(inactiveUnit);
|
|
46
|
+
expect(spies.update).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Success cases
|
|
50
|
+
it("deactivates active non-reference unit", async () => {
|
|
51
|
+
const { db, spies } = createMockDb<DB>();
|
|
52
|
+
const deactivatedUnit = {
|
|
53
|
+
...baseUnitGram,
|
|
54
|
+
isActive: false,
|
|
55
|
+
updatedAt: new Date("2024-01-15T00:00:00.000Z"),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
spies.select.mockReturnValueOnce(baseUnitGram).mockReturnValueOnce(baseUoMCategory);
|
|
59
|
+
spies.update.mockReturnValue(deactivatedUnit);
|
|
60
|
+
|
|
61
|
+
const result = await deactivateUnit(
|
|
62
|
+
db,
|
|
63
|
+
{
|
|
64
|
+
unitId: baseUnitGram.id,
|
|
65
|
+
},
|
|
66
|
+
ctx,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(result.unit.isActive).toBe(false);
|
|
70
|
+
expect(result.unit.updatedAt).not.toBeNull();
|
|
71
|
+
expect(spies.update).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it(
|
|
75
|
+
"throws when permission is missing",
|
|
76
|
+
testPermissionDenied(deactivateUnit, { unitId: "unit-1" }),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -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
|