@tailor-platform/erp-kit 0.3.0 → 0.4.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/CHANGELOG.md +21 -0
- package/README.md +35 -7
- package/dist/cli.mjs +779 -268
- package/package.json +5 -4
- package/skills/erp-kit-app-1-requirements/SKILL.md +8 -9
- package/skills/erp-kit-app-2-requirements-review/references/best-practices-check.md +4 -0
- package/skills/erp-kit-app-2-requirements-review/references/boundary-consistency-check.md +4 -0
- package/skills/erp-kit-app-3-plan/SKILL.md +27 -27
- package/skills/erp-kit-app-3-plan/references/resolver-extraction.md +21 -17
- package/skills/erp-kit-app-3-plan/references/screen-extraction.md +15 -1
- package/skills/erp-kit-app-3-plan/references/story-extraction.md +8 -2
- package/skills/erp-kit-app-5-impl-backend/SKILL.md +9 -11
- package/skills/erp-kit-app-5-impl-backend/references/app-config.md +1 -22
- package/skills/erp-kit-app-5-impl-backend/references/module-wiring.md +0 -1
- package/skills/erp-kit-app-5-impl-backend/references/resolver-patterns.md +13 -4
- package/skills/erp-kit-app-6-impl-frontend/SKILL.md +5 -0
- package/skills/erp-kit-app-6-impl-frontend/references/pages.md +16 -46
- package/skills/erp-kit-app-7-impl-review/SKILL.md +13 -11
- package/skills/erp-kit-app-7-impl-review/references/resolver-doc-code-parity.md +16 -17
- package/skills/erp-kit-module-1-requirements/SKILL.md +6 -12
- package/skills/erp-kit-module-2-requirements-review/SKILL.md +21 -5
- package/skills/erp-kit-module-2-requirements-review/references/requirements-report-format.md +19 -0
- package/skills/erp-kit-module-3-plan/SKILL.md +2 -4
- package/skills/erp-kit-module-4-plan-review/SKILL.md +21 -5
- package/skills/erp-kit-module-4-plan-review/references/parity-report-format.md +15 -0
- package/skills/erp-kit-module-6-impl-review/SKILL.md +21 -7
- package/skills/erp-kit-module-6-impl-review/references/impl-parity-report-format.md +15 -0
- package/skills/erp-kit-module-shared/SKILL.md +4 -0
- package/src/commands/app/index.ts +28 -17
- package/src/commands/check.test.ts +1 -1
- package/src/commands/check.ts +2 -35
- package/src/commands/doc/index.ts +83 -0
- package/src/commands/doc/module.test.ts +119 -0
- package/src/commands/doc/module.ts +114 -0
- package/src/commands/doc/modules.test.ts +103 -0
- package/src/commands/doc/modules.ts +98 -0
- package/src/commands/doc/search.test.ts +94 -0
- package/src/commands/doc/search.ts +111 -0
- package/src/commands/generate-doc.ts +17 -10
- package/src/commands/index.ts +20 -8
- package/src/commands/lib/command-result.ts +30 -0
- package/src/commands/lib/discovery.test.ts +74 -0
- package/src/commands/lib/discovery.ts +106 -0
- package/src/commands/lib/paths.ts +22 -0
- package/src/commands/lib/sync-check-source.test.ts +197 -0
- package/src/commands/lib/sync-check-source.ts +100 -0
- package/src/commands/lib/sync-check-tests.test.ts +178 -0
- package/src/commands/lib/sync-check-tests.ts +69 -0
- package/src/commands/mock/index.ts +11 -6
- package/src/commands/module/generate.ts +13 -8
- package/src/commands/module/index.ts +17 -21
- package/src/commands/parse-doc-test-cases.ts +13 -2
- package/src/commands/sync-check.test.ts +6 -364
- package/src/commands/sync-check.ts +7 -251
- package/src/generator/generate-app-code.test.ts +121 -0
- package/src/generator/generate-app-code.ts +51 -0
- package/src/generator/generate-code-boilerplate.test.ts +1 -1
- package/src/generator/generate-code.test.ts +33 -6
- package/src/generator/generate-code.ts +12 -226
- package/src/generator/generate-errors.ts +34 -0
- package/src/generator/generate-permissions.ts +12 -0
- package/src/generator/generate-shells.ts +28 -0
- package/src/generator/generate-stubs.ts +31 -0
- package/src/generator/parse-resolver-doc.test.ts +89 -0
- package/src/generator/parse-resolver-doc.ts +125 -0
- package/src/generator/scaffold.ts +57 -0
- package/src/generator/stub-templates.test.ts +55 -0
- package/src/generator/stub-templates.ts +145 -0
- package/src/modules/audit/README.md +46 -0
- package/src/modules/audit/command/activateAuditPolicy.generated.ts +6 -0
- package/src/modules/audit/command/activateAuditPolicy.test.ts +186 -0
- package/src/modules/audit/command/activateAuditPolicy.ts +97 -0
- package/src/modules/audit/command/createAuditPolicy.generated.ts +6 -0
- package/src/modules/audit/command/createAuditPolicy.test.ts +395 -0
- package/src/modules/audit/command/createAuditPolicy.ts +131 -0
- package/src/modules/audit/command/deactivateAuditPolicy.generated.ts +6 -0
- package/src/modules/audit/command/deactivateAuditPolicy.test.ts +138 -0
- package/src/modules/audit/command/deactivateAuditPolicy.ts +58 -0
- package/src/modules/audit/command/deleteAuditPolicy.generated.ts +6 -0
- package/src/modules/audit/command/deleteAuditPolicy.test.ts +121 -0
- package/src/modules/audit/command/deleteAuditPolicy.ts +52 -0
- package/src/modules/audit/command/logAuditEvent.generated.ts +6 -0
- package/src/modules/audit/command/logAuditEvent.test.ts +991 -0
- package/src/modules/audit/command/logAuditEvent.ts +357 -0
- package/src/modules/audit/command/reactivateAuditPolicy.generated.ts +6 -0
- package/src/modules/audit/command/reactivateAuditPolicy.test.ts +143 -0
- package/src/modules/audit/command/reactivateAuditPolicy.ts +79 -0
- package/src/modules/audit/command/registerAuditableEntity.generated.ts +6 -0
- package/src/modules/audit/command/registerAuditableEntity.test.ts +268 -0
- package/src/modules/audit/command/registerAuditableEntity.ts +94 -0
- package/src/modules/audit/command/replaceAuditPolicy.generated.ts +6 -0
- package/src/modules/audit/command/replaceAuditPolicy.test.ts +242 -0
- package/src/modules/audit/command/replaceAuditPolicy.ts +91 -0
- package/src/modules/audit/command/updateAuditPolicy.generated.ts +6 -0
- package/src/modules/audit/command/updateAuditPolicy.test.ts +284 -0
- package/src/modules/audit/command/updateAuditPolicy.ts +151 -0
- package/src/modules/audit/db/auditEntry.ts +47 -0
- package/src/modules/audit/db/auditPolicy.ts +33 -0
- package/src/modules/audit/db/auditableEntity.ts +22 -0
- package/src/modules/audit/db/changeDetail.ts +28 -0
- package/src/modules/audit/db/policyFieldRule.ts +23 -0
- package/src/modules/audit/docs/commands/ActivateAuditPolicy.md +69 -0
- package/src/modules/audit/docs/commands/CreateAuditPolicy.md +79 -0
- package/src/modules/audit/docs/commands/DeactivateAuditPolicy.md +55 -0
- package/src/modules/audit/docs/commands/DeleteAuditPolicy.md +55 -0
- package/src/modules/audit/docs/commands/LogAuditEvent.md +137 -0
- package/src/modules/audit/docs/commands/ReactivateAuditPolicy.md +58 -0
- package/src/modules/audit/docs/commands/RegisterAuditableEntity.md +62 -0
- package/src/modules/audit/docs/commands/ReplaceAuditPolicy.md +72 -0
- package/src/modules/audit/docs/commands/UpdateAuditPolicy.md +77 -0
- package/src/modules/audit/docs/features/audit-event-logging.md +126 -0
- package/src/modules/audit/docs/features/audit-policy-configuration.md +135 -0
- package/src/modules/audit/docs/features/field-level-change-tracking.md +95 -0
- package/src/modules/audit/docs/models/AuditEntry.md +55 -0
- package/src/modules/audit/docs/models/AuditPolicy.md +79 -0
- package/src/modules/audit/docs/models/AuditableEntity.md +38 -0
- package/src/modules/audit/docs/models/ChangeDetail.md +55 -0
- package/src/modules/audit/docs/models/PolicyFieldRule.md +45 -0
- package/src/modules/audit/docs/queries/GetAuditEntry.md +49 -0
- package/src/modules/audit/docs/queries/GetAuditPolicy.md +54 -0
- package/src/modules/audit/docs/queries/GetAuditSummary.md +84 -0
- package/src/modules/audit/docs/queries/GetChangeDetails.md +56 -0
- package/src/modules/audit/docs/queries/ListAuditPolicies.md +58 -0
- package/src/modules/audit/docs/queries/SearchAuditEntries.md +91 -0
- package/src/modules/audit/generated/kysely-tailordb.ts +92 -0
- package/src/modules/audit/index.ts +2 -0
- package/src/modules/audit/lib/_db_deps.ts +13 -0
- package/src/modules/audit/lib/errors.generated.ts +120 -0
- package/src/modules/audit/lib/permissions.generated.ts +14 -0
- package/src/modules/audit/lib/types.ts +28 -0
- package/src/modules/audit/module.ts +57 -0
- package/src/modules/audit/permissions.ts +39 -0
- package/src/modules/audit/query/getAuditEntry.generated.ts +5 -0
- package/src/modules/audit/query/getAuditEntry.test.ts +123 -0
- package/src/modules/audit/query/getAuditEntry.ts +36 -0
- package/src/modules/audit/query/getAuditPolicy.generated.ts +5 -0
- package/src/modules/audit/query/getAuditPolicy.test.ts +169 -0
- package/src/modules/audit/query/getAuditPolicy.ts +42 -0
- package/src/modules/audit/query/getAuditSummary.generated.ts +5 -0
- package/src/modules/audit/query/getAuditSummary.test.ts +632 -0
- package/src/modules/audit/query/getAuditSummary.ts +164 -0
- package/src/modules/audit/query/getChangeDetails.generated.ts +5 -0
- package/src/modules/audit/query/getChangeDetails.test.ts +195 -0
- package/src/modules/audit/query/getChangeDetails.ts +42 -0
- package/src/modules/audit/query/listAuditPolicies.generated.ts +5 -0
- package/src/modules/audit/query/listAuditPolicies.test.ts +239 -0
- package/src/modules/audit/query/listAuditPolicies.ts +100 -0
- package/src/modules/audit/query/searchAuditEntries.generated.ts +5 -0
- package/src/modules/audit/query/searchAuditEntries.test.ts +424 -0
- package/src/modules/audit/query/searchAuditEntries.ts +121 -0
- package/src/modules/audit/tailor.config.ts +13 -0
- package/src/modules/audit/tailor.d.ts +13 -0
- package/src/modules/audit/testing/fixtures.ts +215 -0
- package/src/modules/business-partner/README.md +60 -0
- package/src/modules/business-partner/command/.gitkeep +0 -0
- package/src/modules/business-partner/command/activatePartner.generated.ts +6 -0
- package/src/modules/business-partner/command/activatePartner.test.ts +59 -0
- package/src/modules/business-partner/command/activatePartner.ts +45 -0
- package/src/modules/business-partner/command/assignRoleToPartner.generated.ts +6 -0
- package/src/modules/business-partner/command/assignRoleToPartner.test.ts +113 -0
- package/src/modules/business-partner/command/assignRoleToPartner.ts +72 -0
- package/src/modules/business-partner/command/createContactPerson.generated.ts +6 -0
- package/src/modules/business-partner/command/createContactPerson.test.ts +193 -0
- package/src/modules/business-partner/command/createContactPerson.ts +98 -0
- package/src/modules/business-partner/command/createPartner.generated.ts +6 -0
- package/src/modules/business-partner/command/createPartner.test.ts +179 -0
- package/src/modules/business-partner/command/createPartner.ts +83 -0
- package/src/modules/business-partner/command/createPartnerAddress.generated.ts +6 -0
- package/src/modules/business-partner/command/createPartnerAddress.test.ts +195 -0
- package/src/modules/business-partner/command/createPartnerAddress.ts +119 -0
- package/src/modules/business-partner/command/createPartnerBankAccount.generated.ts +6 -0
- package/src/modules/business-partner/command/createPartnerBankAccount.test.ts +297 -0
- package/src/modules/business-partner/command/createPartnerBankAccount.ts +114 -0
- package/src/modules/business-partner/command/createPartnerIdentification.generated.ts +6 -0
- package/src/modules/business-partner/command/createPartnerIdentification.test.ts +255 -0
- package/src/modules/business-partner/command/createPartnerIdentification.ts +97 -0
- package/src/modules/business-partner/command/deactivateContactPerson.generated.ts +6 -0
- package/src/modules/business-partner/command/deactivateContactPerson.test.ts +70 -0
- package/src/modules/business-partner/command/deactivateContactPerson.ts +54 -0
- package/src/modules/business-partner/command/deactivatePartner.generated.ts +6 -0
- package/src/modules/business-partner/command/deactivatePartner.test.ts +59 -0
- package/src/modules/business-partner/command/deactivatePartner.ts +46 -0
- package/src/modules/business-partner/command/deleteContactPerson.generated.ts +6 -0
- package/src/modules/business-partner/command/deleteContactPerson.test.ts +61 -0
- package/src/modules/business-partner/command/deleteContactPerson.ts +48 -0
- package/src/modules/business-partner/command/deletePartner.generated.ts +6 -0
- package/src/modules/business-partner/command/deletePartner.test.ts +58 -0
- package/src/modules/business-partner/command/deletePartner.ts +46 -0
- package/src/modules/business-partner/command/deletePartnerAddress.generated.ts +6 -0
- package/src/modules/business-partner/command/deletePartnerAddress.test.ts +74 -0
- package/src/modules/business-partner/command/deletePartnerAddress.ts +52 -0
- package/src/modules/business-partner/command/deletePartnerBankAccount.generated.ts +6 -0
- package/src/modules/business-partner/command/deletePartnerBankAccount.test.ts +55 -0
- package/src/modules/business-partner/command/deletePartnerBankAccount.ts +36 -0
- package/src/modules/business-partner/command/deletePartnerIdentification.generated.ts +6 -0
- package/src/modules/business-partner/command/deletePartnerIdentification.test.ts +47 -0
- package/src/modules/business-partner/command/deletePartnerIdentification.ts +37 -0
- package/src/modules/business-partner/command/reactivateContactPerson.generated.ts +6 -0
- package/src/modules/business-partner/command/reactivateContactPerson.test.ts +48 -0
- package/src/modules/business-partner/command/reactivateContactPerson.ts +48 -0
- package/src/modules/business-partner/command/reactivatePartner.generated.ts +6 -0
- package/src/modules/business-partner/command/reactivatePartner.test.ts +59 -0
- package/src/modules/business-partner/command/reactivatePartner.ts +46 -0
- package/src/modules/business-partner/command/removeRoleFromPartner.generated.ts +6 -0
- package/src/modules/business-partner/command/removeRoleFromPartner.test.ts +82 -0
- package/src/modules/business-partner/command/removeRoleFromPartner.ts +73 -0
- package/src/modules/business-partner/command/setDefaultPartnerAddress.generated.ts +6 -0
- package/src/modules/business-partner/command/setDefaultPartnerAddress.test.ts +60 -0
- package/src/modules/business-partner/command/setDefaultPartnerAddress.ts +48 -0
- package/src/modules/business-partner/command/setDefaultPartnerBankAccount.generated.ts +6 -0
- package/src/modules/business-partner/command/setDefaultPartnerBankAccount.test.ts +56 -0
- package/src/modules/business-partner/command/setDefaultPartnerBankAccount.ts +51 -0
- package/src/modules/business-partner/command/setPrimaryContactPerson.generated.ts +6 -0
- package/src/modules/business-partner/command/setPrimaryContactPerson.test.ts +63 -0
- package/src/modules/business-partner/command/setPrimaryContactPerson.ts +55 -0
- package/src/modules/business-partner/command/updateContactPerson.generated.ts +6 -0
- package/src/modules/business-partner/command/updateContactPerson.test.ts +193 -0
- package/src/modules/business-partner/command/updateContactPerson.ts +92 -0
- package/src/modules/business-partner/command/updatePartner.generated.ts +6 -0
- package/src/modules/business-partner/command/updatePartner.test.ts +101 -0
- package/src/modules/business-partner/command/updatePartner.ts +76 -0
- package/src/modules/business-partner/command/updatePartnerAddress.generated.ts +6 -0
- package/src/modules/business-partner/command/updatePartnerAddress.test.ts +148 -0
- package/src/modules/business-partner/command/updatePartnerAddress.ts +64 -0
- package/src/modules/business-partner/command/updatePartnerBankAccount.generated.ts +6 -0
- package/src/modules/business-partner/command/updatePartnerBankAccount.test.ts +249 -0
- package/src/modules/business-partner/command/updatePartnerBankAccount.ts +109 -0
- package/src/modules/business-partner/command/updatePartnerIdentification.generated.ts +6 -0
- package/src/modules/business-partner/command/updatePartnerIdentification.test.ts +162 -0
- package/src/modules/business-partner/command/updatePartnerIdentification.ts +105 -0
- package/src/modules/business-partner/db/.gitkeep +0 -0
- package/src/modules/business-partner/db/businessPartner.ts +59 -0
- package/src/modules/business-partner/db/contactPerson.ts +49 -0
- package/src/modules/business-partner/db/partnerAddress.ts +45 -0
- package/src/modules/business-partner/db/partnerBankAccount.ts +53 -0
- package/src/modules/business-partner/db/partnerIdentification.ts +53 -0
- package/src/modules/business-partner/db/partnerRole.ts +43 -0
- package/src/modules/business-partner/docs/commands/ActivatePartner.md +39 -0
- package/src/modules/business-partner/docs/commands/AssignRoleToPartner.md +49 -0
- package/src/modules/business-partner/docs/commands/CreateContactPerson.md +59 -0
- package/src/modules/business-partner/docs/commands/CreatePartner.md +54 -0
- package/src/modules/business-partner/docs/commands/CreatePartnerAddress.md +60 -0
- package/src/modules/business-partner/docs/commands/CreatePartnerBankAccount.md +68 -0
- package/src/modules/business-partner/docs/commands/CreatePartnerIdentification.md +59 -0
- package/src/modules/business-partner/docs/commands/DeactivateContactPerson.md +42 -0
- package/src/modules/business-partner/docs/commands/DeactivatePartner.md +39 -0
- package/src/modules/business-partner/docs/commands/DeleteContactPerson.md +43 -0
- package/src/modules/business-partner/docs/commands/DeletePartner.md +40 -0
- package/src/modules/business-partner/docs/commands/DeletePartnerAddress.md +40 -0
- package/src/modules/business-partner/docs/commands/DeletePartnerBankAccount.md +35 -0
- package/src/modules/business-partner/docs/commands/DeletePartnerIdentification.md +33 -0
- package/src/modules/business-partner/docs/commands/ReactivateContactPerson.md +38 -0
- package/src/modules/business-partner/docs/commands/ReactivatePartner.md +39 -0
- package/src/modules/business-partner/docs/commands/RemoveRoleFromPartner.md +46 -0
- package/src/modules/business-partner/docs/commands/SetDefaultPartnerAddress.md +38 -0
- package/src/modules/business-partner/docs/commands/SetDefaultPartnerBankAccount.md +38 -0
- package/src/modules/business-partner/docs/commands/SetPrimaryContactPerson.md +43 -0
- package/src/modules/business-partner/docs/commands/UpdateContactPerson.md +66 -0
- package/src/modules/business-partner/docs/commands/UpdatePartner.md +48 -0
- package/src/modules/business-partner/docs/commands/UpdatePartnerAddress.md +46 -0
- package/src/modules/business-partner/docs/commands/UpdatePartnerBankAccount.md +64 -0
- package/src/modules/business-partner/docs/commands/UpdatePartnerIdentification.md +52 -0
- package/src/modules/business-partner/docs/features/contact-person-management.md +70 -0
- package/src/modules/business-partner/docs/features/partner-address-management.md +96 -0
- package/src/modules/business-partner/docs/features/partner-bank-account.md +70 -0
- package/src/modules/business-partner/docs/features/partner-identification.md +76 -0
- package/src/modules/business-partner/docs/features/partner-lifecycle.md +59 -0
- package/src/modules/business-partner/docs/features/partner-role-classification.md +73 -0
- package/src/modules/business-partner/docs/models/BusinessPartner.md +64 -0
- package/src/modules/business-partner/docs/models/ContactPerson.md +62 -0
- package/src/modules/business-partner/docs/models/PartnerAddress.md +52 -0
- package/src/modules/business-partner/docs/models/PartnerBankAccount.md +50 -0
- package/src/modules/business-partner/docs/models/PartnerIdentification.md +46 -0
- package/src/modules/business-partner/docs/models/PartnerRole.md +42 -0
- package/src/modules/business-partner/docs/queries/GetContactPerson.md +34 -0
- package/src/modules/business-partner/docs/queries/GetDefaultPartnerAddress.md +40 -0
- package/src/modules/business-partner/docs/queries/GetDefaultPartnerBankAccount.md +36 -0
- package/src/modules/business-partner/docs/queries/GetPartner.md +35 -0
- package/src/modules/business-partner/docs/queries/GetPartnerAddress.md +34 -0
- package/src/modules/business-partner/docs/queries/GetPartnerBankAccount.md +34 -0
- package/src/modules/business-partner/docs/queries/GetPartnerIdentification.md +34 -0
- package/src/modules/business-partner/docs/queries/GetPartnerRole.md +34 -0
- package/src/modules/business-partner/docs/queries/GetPrimaryContactPerson.md +36 -0
- package/src/modules/business-partner/docs/queries/ListContactPersonsByPartner.md +39 -0
- package/src/modules/business-partner/docs/queries/ListPartnerAddressesByPartner.md +41 -0
- package/src/modules/business-partner/docs/queries/ListPartnerBankAccountsByPartner.md +39 -0
- package/src/modules/business-partner/docs/queries/ListPartnerIdentificationsByPartner.md +41 -0
- package/src/modules/business-partner/docs/queries/ListPartnersByRole.md +47 -0
- package/src/modules/business-partner/executor/.gitkeep +0 -0
- package/src/modules/business-partner/generated/.gitkeep +0 -0
- package/src/modules/business-partner/generated/enums.ts +60 -0
- package/src/modules/business-partner/generated/kysely-tailordb.ts +114 -0
- package/src/modules/business-partner/index.ts +2 -0
- package/src/modules/business-partner/lib/_db_deps.ts +17 -0
- package/src/modules/business-partner/lib/errors.generated.ts +172 -0
- package/src/modules/business-partner/lib/errors.ts +2 -0
- package/src/modules/business-partner/lib/permissions.generated.ts +30 -0
- package/src/modules/business-partner/lib/types.ts +53 -0
- package/src/modules/business-partner/module.ts +181 -0
- package/src/modules/business-partner/permissions.ts +3 -0
- package/src/modules/business-partner/query/.gitkeep +0 -0
- package/src/modules/business-partner/query/getContactPerson.generated.ts +5 -0
- package/src/modules/business-partner/query/getContactPerson.test.ts +31 -0
- package/src/modules/business-partner/query/getContactPerson.ts +16 -0
- package/src/modules/business-partner/query/getDefaultPartnerAddress.generated.ts +5 -0
- package/src/modules/business-partner/query/getDefaultPartnerAddress.test.ts +45 -0
- package/src/modules/business-partner/query/getDefaultPartnerAddress.ts +30 -0
- package/src/modules/business-partner/query/getDefaultPartnerBankAccount.generated.ts +5 -0
- package/src/modules/business-partner/query/getDefaultPartnerBankAccount.test.ts +43 -0
- package/src/modules/business-partner/query/getDefaultPartnerBankAccount.ts +17 -0
- package/src/modules/business-partner/query/getPartner.generated.ts +5 -0
- package/src/modules/business-partner/query/getPartner.test.ts +31 -0
- package/src/modules/business-partner/query/getPartner.ts +16 -0
- package/src/modules/business-partner/query/getPartnerAddress.generated.ts +5 -0
- package/src/modules/business-partner/query/getPartnerAddress.test.ts +31 -0
- package/src/modules/business-partner/query/getPartnerAddress.ts +16 -0
- package/src/modules/business-partner/query/getPartnerBankAccount.generated.ts +5 -0
- package/src/modules/business-partner/query/getPartnerBankAccount.test.ts +31 -0
- package/src/modules/business-partner/query/getPartnerBankAccount.ts +16 -0
- package/src/modules/business-partner/query/getPartnerIdentification.generated.ts +5 -0
- package/src/modules/business-partner/query/getPartnerIdentification.test.ts +31 -0
- package/src/modules/business-partner/query/getPartnerIdentification.ts +16 -0
- package/src/modules/business-partner/query/getPartnerRole.generated.ts +5 -0
- package/src/modules/business-partner/query/getPartnerRole.test.ts +31 -0
- package/src/modules/business-partner/query/getPartnerRole.ts +19 -0
- package/src/modules/business-partner/query/getPrimaryContactPerson.generated.ts +5 -0
- package/src/modules/business-partner/query/getPrimaryContactPerson.test.ts +43 -0
- package/src/modules/business-partner/query/getPrimaryContactPerson.ts +17 -0
- package/src/modules/business-partner/query/listContactPersonsByPartner.generated.ts +5 -0
- package/src/modules/business-partner/query/listContactPersonsByPartner.test.ts +77 -0
- package/src/modules/business-partner/query/listContactPersonsByPartner.ts +32 -0
- package/src/modules/business-partner/query/listPartnerAddressesByPartner.generated.ts +5 -0
- package/src/modules/business-partner/query/listPartnerAddressesByPartner.test.ts +71 -0
- package/src/modules/business-partner/query/listPartnerAddressesByPartner.ts +37 -0
- package/src/modules/business-partner/query/listPartnerBankAccountsByPartner.generated.ts +5 -0
- package/src/modules/business-partner/query/listPartnerBankAccountsByPartner.test.ts +59 -0
- package/src/modules/business-partner/query/listPartnerBankAccountsByPartner.ts +32 -0
- package/src/modules/business-partner/query/listPartnerIdentificationsByPartner.generated.ts +5 -0
- package/src/modules/business-partner/query/listPartnerIdentificationsByPartner.test.ts +72 -0
- package/src/modules/business-partner/query/listPartnerIdentificationsByPartner.ts +40 -0
- package/src/modules/business-partner/query/listPartnersByRole.generated.ts +5 -0
- package/src/modules/business-partner/query/listPartnersByRole.test.ts +103 -0
- package/src/modules/business-partner/query/listPartnersByRole.ts +47 -0
- package/src/modules/business-partner/tailor.config.ts +13 -0
- package/src/modules/business-partner/tailor.d.ts +13 -0
- package/src/modules/business-partner/testing/fixtures.ts +204 -0
- package/src/modules/coa-management/README.md +61 -0
- package/src/modules/coa-management/command/.gitkeep +0 -0
- package/src/modules/coa-management/command/activateAccount.generated.ts +6 -0
- package/src/modules/coa-management/command/activateAccount.test.ts +125 -0
- package/src/modules/coa-management/command/activateAccount.ts +105 -0
- package/src/modules/coa-management/command/activateChartOfAccounts.generated.ts +6 -0
- package/src/modules/coa-management/command/activateChartOfAccounts.test.ts +113 -0
- package/src/modules/coa-management/command/activateChartOfAccounts.ts +104 -0
- package/src/modules/coa-management/command/createAccount.generated.ts +6 -0
- package/src/modules/coa-management/command/createAccount.test.ts +767 -0
- package/src/modules/coa-management/command/createAccount.ts +247 -0
- package/src/modules/coa-management/command/createAccountGroup.generated.ts +6 -0
- package/src/modules/coa-management/command/createAccountGroup.test.ts +494 -0
- package/src/modules/coa-management/command/createAccountGroup.ts +207 -0
- package/src/modules/coa-management/command/createChartOfAccounts.generated.ts +6 -0
- package/src/modules/coa-management/command/createChartOfAccounts.test.ts +502 -0
- package/src/modules/coa-management/command/createChartOfAccounts.ts +267 -0
- package/src/modules/coa-management/command/deactivateAccount.generated.ts +6 -0
- package/src/modules/coa-management/command/deactivateAccount.test.ts +199 -0
- package/src/modules/coa-management/command/deactivateAccount.ts +142 -0
- package/src/modules/coa-management/command/deactivateChartOfAccounts.generated.ts +6 -0
- package/src/modules/coa-management/command/deactivateChartOfAccounts.test.ts +91 -0
- package/src/modules/coa-management/command/deactivateChartOfAccounts.ts +88 -0
- package/src/modules/coa-management/command/deleteAccount.generated.ts +6 -0
- package/src/modules/coa-management/command/deleteAccount.test.ts +122 -0
- package/src/modules/coa-management/command/deleteAccount.ts +103 -0
- package/src/modules/coa-management/command/deleteAccountGroup.generated.ts +6 -0
- package/src/modules/coa-management/command/deleteAccountGroup.test.ts +120 -0
- package/src/modules/coa-management/command/deleteAccountGroup.ts +113 -0
- package/src/modules/coa-management/command/deleteChartOfAccounts.generated.ts +6 -0
- package/src/modules/coa-management/command/deleteChartOfAccounts.test.ts +154 -0
- package/src/modules/coa-management/command/deleteChartOfAccounts.ts +133 -0
- package/src/modules/coa-management/command/moveAccountGroup.generated.ts +6 -0
- package/src/modules/coa-management/command/moveAccountGroup.test.ts +199 -0
- package/src/modules/coa-management/command/moveAccountGroup.ts +145 -0
- package/src/modules/coa-management/command/reactivateAccount.generated.ts +6 -0
- package/src/modules/coa-management/command/reactivateAccount.test.ts +126 -0
- package/src/modules/coa-management/command/reactivateAccount.ts +123 -0
- package/src/modules/coa-management/command/updateAccount.generated.ts +6 -0
- package/src/modules/coa-management/command/updateAccount.test.ts +669 -0
- package/src/modules/coa-management/command/updateAccount.ts +370 -0
- package/src/modules/coa-management/command/updateAccountGroup.generated.ts +6 -0
- package/src/modules/coa-management/command/updateAccountGroup.test.ts +253 -0
- package/src/modules/coa-management/command/updateAccountGroup.ts +191 -0
- package/src/modules/coa-management/command/updateChartOfAccounts.generated.ts +6 -0
- package/src/modules/coa-management/command/updateChartOfAccounts.test.ts +153 -0
- package/src/modules/coa-management/command/updateChartOfAccounts.ts +133 -0
- package/src/modules/coa-management/db/.gitkeep +0 -0
- package/src/modules/coa-management/db/account.ts +119 -0
- package/src/modules/coa-management/db/accountGroup.ts +57 -0
- package/src/modules/coa-management/db/chartOfAccounts.ts +55 -0
- package/src/modules/coa-management/docs/commands/ActivateAccount.md +49 -0
- package/src/modules/coa-management/docs/commands/ActivateChartOfAccounts.md +47 -0
- package/src/modules/coa-management/docs/commands/CreateAccount.md +94 -0
- package/src/modules/coa-management/docs/commands/CreateAccountGroup.md +70 -0
- package/src/modules/coa-management/docs/commands/CreateChartOfAccounts.md +72 -0
- package/src/modules/coa-management/docs/commands/DeactivateAccount.md +65 -0
- package/src/modules/coa-management/docs/commands/DeactivateChartOfAccounts.md +44 -0
- package/src/modules/coa-management/docs/commands/DeleteAccount.md +52 -0
- package/src/modules/coa-management/docs/commands/DeleteAccountGroup.md +50 -0
- package/src/modules/coa-management/docs/commands/DeleteChartOfAccounts.md +48 -0
- package/src/modules/coa-management/docs/commands/MoveAccountGroup.md +57 -0
- package/src/modules/coa-management/docs/commands/ReactivateAccount.md +50 -0
- package/src/modules/coa-management/docs/commands/UpdateAccount.md +102 -0
- package/src/modules/coa-management/docs/commands/UpdateAccountGroup.md +62 -0
- package/src/modules/coa-management/docs/commands/UpdateChartOfAccounts.md +49 -0
- package/src/modules/coa-management/docs/features/account-group-hierarchy.md +81 -0
- package/src/modules/coa-management/docs/features/account-lifecycle.md +80 -0
- package/src/modules/coa-management/docs/features/account-management.md +114 -0
- package/src/modules/coa-management/docs/features/chart-of-accounts-setup.md +86 -0
- package/src/modules/coa-management/docs/models/Account.md +84 -0
- package/src/modules/coa-management/docs/models/AccountGroup.md +55 -0
- package/src/modules/coa-management/docs/models/ChartOfAccounts.md +65 -0
- package/src/modules/coa-management/docs/queries/DetectCircularReference.md +52 -0
- package/src/modules/coa-management/docs/queries/GetAccount.md +42 -0
- package/src/modules/coa-management/docs/queries/GetAccountGroup.md +42 -0
- package/src/modules/coa-management/docs/queries/GetChartOfAccounts.md +48 -0
- package/src/modules/coa-management/docs/queries/ListAccountGroups.md +42 -0
- package/src/modules/coa-management/docs/queries/ListAccounts.md +54 -0
- package/src/modules/coa-management/docs/queries/ListUnassignedAccounts.md +40 -0
- package/src/modules/coa-management/executor/.gitkeep +0 -0
- package/src/modules/coa-management/generated/.gitkeep +0 -0
- package/src/modules/coa-management/generated/enums.ts +45 -0
- package/src/modules/coa-management/generated/kysely-tailordb.ts +81 -0
- package/src/modules/coa-management/index.ts +2 -0
- package/src/modules/coa-management/lib/_db_deps.ts +17 -0
- package/src/modules/coa-management/lib/errors.generated.ts +162 -0
- package/src/modules/coa-management/lib/errors.ts +0 -0
- package/src/modules/coa-management/lib/permissions.generated.ts +20 -0
- package/src/modules/coa-management/lib/types.ts +22 -0
- package/src/modules/coa-management/module.ts +136 -0
- package/src/modules/coa-management/permissions.ts +3 -0
- package/src/modules/coa-management/query/.gitkeep +0 -0
- package/src/modules/coa-management/query/detectCircularReference.generated.ts +5 -0
- package/src/modules/coa-management/query/detectCircularReference.test.ts +88 -0
- package/src/modules/coa-management/query/detectCircularReference.ts +46 -0
- package/src/modules/coa-management/query/getAccount.generated.ts +5 -0
- package/src/modules/coa-management/query/getAccount.test.ts +55 -0
- package/src/modules/coa-management/query/getAccount.ts +25 -0
- package/src/modules/coa-management/query/getAccountGroup.generated.ts +5 -0
- package/src/modules/coa-management/query/getAccountGroup.test.ts +55 -0
- package/src/modules/coa-management/query/getAccountGroup.ts +25 -0
- package/src/modules/coa-management/query/getChartOfAccounts.generated.ts +5 -0
- package/src/modules/coa-management/query/getChartOfAccounts.test.ts +79 -0
- package/src/modules/coa-management/query/getChartOfAccounts.ts +28 -0
- package/src/modules/coa-management/query/listAccountGroups.generated.ts +5 -0
- package/src/modules/coa-management/query/listAccountGroups.test.ts +72 -0
- package/src/modules/coa-management/query/listAccountGroups.ts +49 -0
- package/src/modules/coa-management/query/listAccounts.generated.ts +5 -0
- package/src/modules/coa-management/query/listAccounts.test.ts +136 -0
- package/src/modules/coa-management/query/listAccounts.ts +82 -0
- package/src/modules/coa-management/query/listUnassignedAccounts.generated.ts +5 -0
- package/src/modules/coa-management/query/listUnassignedAccounts.test.ts +96 -0
- package/src/modules/coa-management/query/listUnassignedAccounts.ts +39 -0
- package/src/modules/coa-management/tailor.config.ts +13 -0
- package/src/modules/coa-management/tailor.d.ts +13 -0
- package/src/modules/coa-management/testing/fixtures.ts +201 -0
- package/src/modules/item-management/README.md +1 -1
- package/src/modules/organization/README.md +57 -0
- package/src/modules/organization/command/.gitkeep +0 -0
- package/src/modules/organization/command/activateCompany.generated.ts +6 -0
- package/src/modules/organization/command/activateCompany.test.ts +184 -0
- package/src/modules/organization/command/activateCompany.ts +92 -0
- package/src/modules/organization/command/createCompany.generated.ts +6 -0
- package/src/modules/organization/command/createCompany.test.ts +156 -0
- package/src/modules/organization/command/createCompany.ts +80 -0
- package/src/modules/organization/command/createDepartment.generated.ts +6 -0
- package/src/modules/organization/command/createDepartment.test.ts +239 -0
- package/src/modules/organization/command/createDepartment.ts +98 -0
- package/src/modules/organization/command/createSite.generated.ts +6 -0
- package/src/modules/organization/command/createSite.test.ts +262 -0
- package/src/modules/organization/command/createSite.ts +155 -0
- package/src/modules/organization/command/deactivateCompany.generated.ts +6 -0
- package/src/modules/organization/command/deactivateCompany.test.ts +58 -0
- package/src/modules/organization/command/deactivateCompany.ts +47 -0
- package/src/modules/organization/command/deactivateDepartment.generated.ts +6 -0
- package/src/modules/organization/command/deactivateDepartment.test.ts +115 -0
- package/src/modules/organization/command/deactivateDepartment.ts +63 -0
- package/src/modules/organization/command/deactivateSite.generated.ts +6 -0
- package/src/modules/organization/command/deactivateSite.test.ts +53 -0
- package/src/modules/organization/command/deactivateSite.ts +47 -0
- package/src/modules/organization/command/deleteCompany.generated.ts +6 -0
- package/src/modules/organization/command/deleteCompany.test.ts +99 -0
- package/src/modules/organization/command/deleteCompany.ts +66 -0
- package/src/modules/organization/command/reactivateCompany.generated.ts +6 -0
- package/src/modules/organization/command/reactivateCompany.test.ts +58 -0
- package/src/modules/organization/command/reactivateCompany.ts +47 -0
- package/src/modules/organization/command/reactivateDepartment.generated.ts +6 -0
- package/src/modules/organization/command/reactivateDepartment.test.ts +59 -0
- package/src/modules/organization/command/reactivateDepartment.ts +47 -0
- package/src/modules/organization/command/reactivateSite.generated.ts +6 -0
- package/src/modules/organization/command/reactivateSite.test.ts +53 -0
- package/src/modules/organization/command/reactivateSite.ts +47 -0
- package/src/modules/organization/command/updateCompany.generated.ts +6 -0
- package/src/modules/organization/command/updateCompany.test.ts +239 -0
- package/src/modules/organization/command/updateCompany.ts +127 -0
- package/src/modules/organization/command/updateDepartment.generated.ts +6 -0
- package/src/modules/organization/command/updateDepartment.test.ts +232 -0
- package/src/modules/organization/command/updateDepartment.ts +120 -0
- package/src/modules/organization/command/updateSite.generated.ts +6 -0
- package/src/modules/organization/command/updateSite.test.ts +274 -0
- package/src/modules/organization/command/updateSite.ts +176 -0
- package/src/modules/organization/db/.gitkeep +0 -0
- package/src/modules/organization/db/company.ts +44 -0
- package/src/modules/organization/db/department.ts +46 -0
- package/src/modules/organization/db/site.ts +44 -0
- package/src/modules/organization/docs/commands/ActivateCompany.md +62 -0
- package/src/modules/organization/docs/commands/CreateCompany.md +49 -0
- package/src/modules/organization/docs/commands/CreateDepartment.md +62 -0
- package/src/modules/organization/docs/commands/CreateSite.md +74 -0
- package/src/modules/organization/docs/commands/DeactivateCompany.md +40 -0
- package/src/modules/organization/docs/commands/DeactivateDepartment.md +44 -0
- package/src/modules/organization/docs/commands/DeactivateSite.md +38 -0
- package/src/modules/organization/docs/commands/DeleteCompany.md +50 -0
- package/src/modules/organization/docs/commands/ReactivateCompany.md +39 -0
- package/src/modules/organization/docs/commands/ReactivateDepartment.md +37 -0
- package/src/modules/organization/docs/commands/ReactivateSite.md +37 -0
- package/src/modules/organization/docs/commands/UpdateCompany.md +58 -0
- package/src/modules/organization/docs/commands/UpdateDepartment.md +64 -0
- package/src/modules/organization/docs/commands/UpdateSite.md +80 -0
- package/src/modules/organization/docs/features/company-lifecycle.md +76 -0
- package/src/modules/organization/docs/features/department-management.md +66 -0
- package/src/modules/organization/docs/features/site-management.md +86 -0
- package/src/modules/organization/docs/models/Company.md +60 -0
- package/src/modules/organization/docs/models/Department.md +57 -0
- package/src/modules/organization/docs/models/Site.md +57 -0
- package/src/modules/organization/docs/queries/DetectDepartmentCircularReference.md +50 -0
- package/src/modules/organization/docs/queries/GetCompany.md +40 -0
- package/src/modules/organization/docs/queries/GetDepartment.md +44 -0
- package/src/modules/organization/docs/queries/GetDepartmentChildren.md +40 -0
- package/src/modules/organization/docs/queries/GetSite.md +37 -0
- package/src/modules/organization/docs/queries/ListDepartmentsByCompany.md +54 -0
- package/src/modules/organization/docs/queries/ListSitesByCompany.md +54 -0
- package/src/modules/organization/executor/.gitkeep +0 -0
- package/src/modules/organization/generated/.gitkeep +0 -0
- package/src/modules/organization/generated/kysely-tailordb.ts +77 -0
- package/src/modules/organization/index.ts +2 -0
- package/src/modules/organization/lib/_db_deps.ts +10 -0
- package/src/modules/organization/lib/errors.generated.ts +117 -0
- package/src/modules/organization/lib/errors.ts +1 -0
- package/src/modules/organization/lib/permissions.generated.ts +19 -0
- package/src/modules/organization/lib/types.ts +16 -0
- package/src/modules/organization/module.ts +89 -0
- package/src/modules/organization/permissions.ts +3 -0
- package/src/modules/organization/query/.gitkeep +0 -0
- package/src/modules/organization/query/detectDepartmentCircularReference.generated.ts +5 -0
- package/src/modules/organization/query/detectDepartmentCircularReference.test.ts +102 -0
- package/src/modules/organization/query/detectDepartmentCircularReference.ts +27 -0
- package/src/modules/organization/query/getCompany.generated.ts +5 -0
- package/src/modules/organization/query/getCompany.test.ts +70 -0
- package/src/modules/organization/query/getCompany.ts +16 -0
- package/src/modules/organization/query/getDepartment.generated.ts +5 -0
- package/src/modules/organization/query/getDepartment.test.ts +85 -0
- package/src/modules/organization/query/getDepartment.ts +17 -0
- package/src/modules/organization/query/getDepartmentChildren.generated.ts +5 -0
- package/src/modules/organization/query/getDepartmentChildren.test.ts +75 -0
- package/src/modules/organization/query/getDepartmentChildren.ts +21 -0
- package/src/modules/organization/query/getSite.generated.ts +5 -0
- package/src/modules/organization/query/getSite.test.ts +55 -0
- package/src/modules/organization/query/getSite.ts +16 -0
- package/src/modules/organization/query/listDepartmentsByCompany.generated.ts +5 -0
- package/src/modules/organization/query/listDepartmentsByCompany.test.ts +124 -0
- package/src/modules/organization/query/listDepartmentsByCompany.ts +43 -0
- package/src/modules/organization/query/listSitesByCompany.generated.ts +5 -0
- package/src/modules/organization/query/listSitesByCompany.test.ts +126 -0
- package/src/modules/organization/query/listSitesByCompany.ts +41 -0
- package/src/modules/organization/tailor.config.ts +13 -0
- package/src/modules/organization/tailor.d.ts +13 -0
- package/src/modules/organization/testing/fixtures.ts +155 -0
- package/src/modules/primitives/README.md +1 -1
- package/src/modules/primitives/command/setBaseCurrency.test.ts +8 -64
- package/src/modules/primitives/command/setBaseCurrency.ts +6 -64
- package/src/modules/primitives/docs/commands/ActivateCategory.md +1 -1
- package/src/modules/primitives/docs/commands/ActivateCurrency.md +1 -1
- package/src/modules/primitives/docs/commands/ActivateUnit.md +1 -1
- package/src/modules/primitives/docs/commands/CreateExchangeRate.md +2 -2
- package/src/modules/primitives/docs/commands/CreateUnit.md +1 -1
- package/src/modules/primitives/docs/commands/DeactivateCategory.md +1 -1
- package/src/modules/primitives/docs/commands/DeactivateCurrency.md +1 -1
- package/src/modules/primitives/docs/commands/DeactivateUnit.md +1 -1
- package/src/modules/primitives/docs/commands/SetBaseCurrency.md +13 -23
- package/src/modules/primitives/docs/commands/SetReferenceUnit.md +1 -1
- package/src/modules/primitives/docs/features/currency-definitions.md +13 -14
- package/src/modules/primitives/docs/models/Currency.md +3 -4
- package/src/modules/primitives/docs/queries/ConvertAmount.md +2 -2
- package/src/modules/primitives/docs/queries/ConvertQuantity.md +2 -2
- package/src/modules/primitives/lib/errors.generated.ts +5 -0
- package/src/modules/product-management/README.md +1 -1
- package/src/modules/user-management/docs/commands/CreatePermission.md +3 -3
- package/src/modules/user-management/docs/commands/CreateRole.md +1 -1
- package/src/modules/user-management/docs/queries/ListRolePermissionsByRole.md +39 -0
- package/src/modules/user-management/docs/queries/ListUserRolesByUser.md +39 -0
- package/src/modules/user-management/generated/enums.ts +0 -15
- package/src/modules/user-management/generated/kysely-tailordb.ts +0 -11
- package/src/shared/createContext.ts +2 -1
- package/src/shared/defineQuery.ts +36 -1
- package/src/shared/requirePermission.ts +3 -3
- package/src/shared/types.ts +3 -0
- package/templates/scaffold/app/backend/package.json +4 -3
- package/templates/scaffold/app/frontend/eslint.config.js +12 -0
- package/templates/scaffold/app/frontend/package.json +10 -7
- package/templates/scaffold/app/frontend/src/hooks/use-toast.ts +30 -0
- package/templates/scaffold/app/frontend/src/pages/user-management/user/create/components/create-user-form.tsx +3 -2
- package/templates/scaffold/app/frontend/vite.config.ts +5 -5
- package/src/commands/module/list.test.ts +0 -57
- package/src/commands/module/list.ts +0 -64
- /package/src/modules/{accounting → audit/db}/.gitkeep +0 -0
- /package/src/modules/audit/{.gitkeep → executor/.gitkeep} +0 -0
- /package/src/modules/{coa-management/.gitkeep → audit/lib/errors.ts} +0 -0
- /package/src/modules/{supplier-management → business-partner}/.gitkeep +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Audit Event Logging
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Audit Event Logging captures create, update, and delete operations performed across all ERP modules as immutable audit entries. Each entry records a unique `eventId` for deduplication and idempotency, an optional `correlationId` for grouping related events within a logical operation (e.g., bulk updates, cascading workflows, saga steps), the actor identity (actorType and actorId, with optional metadata such as IP address, user agent, sessionId, requestId, or onBehalfOf reference), the operation type (CREATE, UPDATE, DELETE), the affected entity reference (entityType and entityId), and a precise timestamp. Entries may optionally be scoped by companyId for multi-company data isolation; entries for global entities (e.g., user accounts, currencies) omit companyId and are queryable without company scope.
|
|
6
|
+
|
|
7
|
+
Audit entries are strictly append-only — once written, they cannot be modified or deleted. This immutability guarantee is fundamental to compliance, forensic investigation, and regulatory reporting. The audit log serves as a universal change history that is queryable by actor, entity type, specific record, operation type, and date range, enabling both real-time monitoring and historical analysis of system activity.
|
|
8
|
+
|
|
9
|
+
Other modules emit audit events by calling the audit module's ingestion interface. Every audit event submitted must include: `eventId` (UUID, unique, used for idempotency), `actorType` (USER/SYSTEM/SERVICE), `actorId`, `entityType` (must match a registered auditable entity), `entityId`, `operationType` (CREATE/UPDATE/DELETE), and `changes` (array of ChangeDetail). Optional fields include: `correlationId` (UUID, groups related events from a single logical operation), `onBehalfOf` (delegated userId), `actorMetadata` (ipAddress, userAgent, sessionId, requestId), and `companyId` (required for company-bound entities, must be null for global entities). The timestamp is always system-generated at ingestion time and cannot be supplied or overridden by the caller, ensuring a consistent and tamper-resistant time source.
|
|
10
|
+
|
|
11
|
+
The audit module enforces idempotency on `eventId` — submitting the same eventId more than once silently discards the duplicate (returns success, no new entry). This allows emitting modules to safely retry on transient failures. Ingestion is eventually consistent with the source operation: emitting modules complete their own database transaction first, then submit the audit event. The audit module does not participate in the emitting module's transaction, preventing audit failures from blocking business operations. If the audit write fails permanently after retries, the emitting module logs the failure for operational alerting. Duplicate eventIds are silently accepted; missing required fields, unknown entityTypes, and scope mismatches are rejected with validation errors; events for entity + operation combinations with no active policy are silently discarded.
|
|
12
|
+
|
|
13
|
+
Modules that want their entities to be auditable register them with the audit module at initialization time, declaring `entityName`, `entityScope` (COMPANY_BOUND or GLOBAL), and `auditableFields` (list of field names eligible for audit capture). The audit module validates that `entityType` in incoming events matches a registered entity.
|
|
14
|
+
|
|
15
|
+
## Business Purpose
|
|
16
|
+
|
|
17
|
+
Audit Event Logging provides an append-only record of data mutations captured by the system. Because ingestion is eventually consistent and decoupled from source transactions, coverage is best-effort — not every mutation is guaranteed to be recorded (see ingestion contract above). However, once an entry is written it is immutable and cannot be altered or deleted. This enables:
|
|
18
|
+
|
|
19
|
+
- Regulatory compliance: immutable, append-only logs support SOX, GDPR, and industry-specific audit trail requirements (applications requiring guaranteed capture should layer an outbox or replay mechanism on top)
|
|
20
|
+
- Forensic investigation: security teams can trace exactly who or what changed data, when, and from where
|
|
21
|
+
- Operational accountability: managers can review change history for any business record to understand how it reached its current state
|
|
22
|
+
- Multi-company isolation: company-scoped audit entries ensure each legal entity's audit trail is independently queryable
|
|
23
|
+
- Global entity auditing: changes to foundational, non-company-scoped entities (users, currencies, UoM) are captured without requiring a company context
|
|
24
|
+
- Change attribution: every mutation is linked to a specific actor identity (human user, system process, or service account), eliminating ambiguity about responsibility
|
|
25
|
+
|
|
26
|
+
The append-only constraint on stored entries ensures:
|
|
27
|
+
|
|
28
|
+
- Historical integrity of recorded entries is preserved — no entry can be retroactively altered or removed
|
|
29
|
+
- Stored audit trails remain admissible for external auditors and regulators
|
|
30
|
+
- System administrators cannot suppress evidence of recorded operations
|
|
31
|
+
|
|
32
|
+
## Process Flow
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart TD
|
|
36
|
+
A[Actor Performs Operation] --> B[System Identifies Operation Type]
|
|
37
|
+
B --> C{Operation Type}
|
|
38
|
+
C -->|CREATE| D[Capture New Entity State]
|
|
39
|
+
C -->|UPDATE| E[Capture Changed Entity State]
|
|
40
|
+
C -->|DELETE| F[Capture Deleted Entity Reference]
|
|
41
|
+
D --> G[Resolve Actor Identity]
|
|
42
|
+
E --> G
|
|
43
|
+
F --> G
|
|
44
|
+
G --> H[Record actorType + actorId + Metadata]
|
|
45
|
+
H --> I[Determine Scope: company or global]
|
|
46
|
+
I --> J[Build AuditEntry]
|
|
47
|
+
J --> K[Write Immutable Entry]
|
|
48
|
+
K --> L[Entry Available for Query]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Querying the audit log follows this pattern. All query paths enforce authorization — the caller must hold `viewAuditHistory` (for detail queries) or `viewAuditSummary` (for aggregate queries) at the appropriate scope:
|
|
52
|
+
|
|
53
|
+
```mermaid
|
|
54
|
+
flowchart TD
|
|
55
|
+
A[Query Request] --> AA{Caller holds required permission?}
|
|
56
|
+
AA -->|No| AB[Reject: insufficient permissions]
|
|
57
|
+
AA -->|Yes| B{Filter Criteria}
|
|
58
|
+
B --> C[By Actor: actorType + actorId]
|
|
59
|
+
B --> D[By Entity Type + Entity ID]
|
|
60
|
+
B --> E[By Operation Type]
|
|
61
|
+
B --> F[By Date Range]
|
|
62
|
+
C --> G{Scope Filter}
|
|
63
|
+
D --> G
|
|
64
|
+
E --> G
|
|
65
|
+
F --> G
|
|
66
|
+
G -->|companyId provided| H[Return company-scoped entries\nif caller has company-scoped permission]
|
|
67
|
+
G -->|no companyId| I[Return global entries only\nif caller has global-scoped permission]
|
|
68
|
+
G -->|both requested| J[Return combined results\nfor scopes caller is authorized for]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Scenario Patterns
|
|
72
|
+
|
|
73
|
+
- **Record Creation Tracking**: A user creates a new sales order. The system writes an AuditEntry with operation CREATE, actorType = USER, the actor's actorId (userId), the entityType (e.g., SalesOrder), the new record's entityId, companyId, and a timestamp — providing a permanent record of who created the order and when
|
|
74
|
+
- **Field Update History**: A user modifies the status of a purchase order. An AuditEntry with operation UPDATE is appended, capturing the actor identity and the affected entity reference, enabling reviewers to see the full change timeline for that record
|
|
75
|
+
- **Deletion Accountability**: An operator deletes a draft invoice. An AuditEntry with operation DELETE is written, ensuring the deletion is permanently recorded even though the original record no longer exists
|
|
76
|
+
- **System-Initiated Change**: A scheduled job recalculates exchange rates. The system writes an AuditEntry with actorType = SYSTEM, actorId referencing the job identifier, and no companyId (global scope), capturing automated changes with the same fidelity as human-initiated ones
|
|
77
|
+
- **Delegated Action**: An administrator performs an action on behalf of another user. The AuditEntry records actorType = USER, actorId = the administrator's userId, and onBehalfOf = the target user's userId, preserving the full delegation chain
|
|
78
|
+
- **Security Investigation**: A security team investigates suspicious activity by querying all audit entries for a specific actorId within a date range, across all operation types, to reconstruct the actor's actions
|
|
79
|
+
- **Compliance Audit**: An external auditor requests the full change history for a specific entity (e.g., all mutations to a particular general ledger account) by querying audit entries filtered by entityType and entityId within a fiscal period
|
|
80
|
+
- **Multi-Company Audit Isolation**: A parent organization runs an audit for one subsidiary. Queries are scoped by companyId, returning only audit entries belonging to that legal entity without leaking data from other companies
|
|
81
|
+
- **Global Entity Audit**: An auditor reviews all changes to user accounts (a non-company-scoped entity). Queries omit companyId to retrieve the full global change history for user-management entities
|
|
82
|
+
|
|
83
|
+
## Test Cases
|
|
84
|
+
|
|
85
|
+
- Every accepted audit event for an entity and operation type that has an active policy produces exactly one AuditEntry — except UPDATE events with an empty changes array (zero-delta), which are silently accepted as a no-op without creating an AuditEntry
|
|
86
|
+
- AuditEntry contains required fields: eventId, actorType, actorId, entityType, entityId, operationType, and timestamp
|
|
87
|
+
- companyId is required for company-scoped entities and optional (null) for global entities
|
|
88
|
+
- actorType is required and must be one of USER, SYSTEM, or SERVICE
|
|
89
|
+
- actorId is required and must be non-empty regardless of actorType
|
|
90
|
+
- onBehalfOf is optional and stores the delegated user's identity when an action is performed on behalf of another actor
|
|
91
|
+
- AuditEntries cannot be modified after creation — update attempts are rejected
|
|
92
|
+
- AuditEntries cannot be deleted — delete attempts are rejected
|
|
93
|
+
- Actor metadata (IP address, user agent) is optional and stored when provided
|
|
94
|
+
- Querying by actorId returns only entries for that actor
|
|
95
|
+
- Querying by entityType and entityId returns the full change history for a specific record
|
|
96
|
+
- Querying by operationType filters entries to only the specified operation (CREATE, UPDATE, or DELETE)
|
|
97
|
+
- Querying by date range returns only entries whose timestamp falls within the range
|
|
98
|
+
- Querying with companyId returns only entries scoped to that company
|
|
99
|
+
- Querying without companyId returns only global (company-unscoped) entries
|
|
100
|
+
- An operation performed without a valid actor identity (actorType + actorId) is rejected
|
|
101
|
+
- Timestamp is system-generated and cannot be overridden by the caller
|
|
102
|
+
- Audit entries are returned in chronological order by default
|
|
103
|
+
- Large result sets support pagination for efficient retrieval
|
|
104
|
+
- Every audit event must include a unique eventId (UUID)
|
|
105
|
+
- Submitting an event with a duplicate eventId is silently accepted without creating a new entry
|
|
106
|
+
- correlationId is optional and links related events from a single logical operation
|
|
107
|
+
- Events referencing an unregistered entityType are rejected with a validation error
|
|
108
|
+
- Events with companyId on a global entity are rejected with a validation error
|
|
109
|
+
- Events with null companyId on a company-bound entity are rejected with a validation error
|
|
110
|
+
- Events for an entity + operation with no active policy are silently discarded
|
|
111
|
+
- Entity registration requires entityName, entityScope, and auditableFields
|
|
112
|
+
- actorMetadata (ipAddress, userAgent, sessionId, requestId) is optional and stored when provided
|
|
113
|
+
- Querying audit entries requires the `viewAuditHistory` permission at the appropriate scope
|
|
114
|
+
- A caller with company-scoped `viewAuditHistory` can only retrieve entries for their assigned companies
|
|
115
|
+
- A caller with global-scoped `viewAuditHistory` can retrieve global (company-unscoped) entries
|
|
116
|
+
- A caller without `viewAuditHistory` permission receives an authorization error when querying audit entries
|
|
117
|
+
- Querying audit summaries requires the `viewAuditSummary` permission at the appropriate scope
|
|
118
|
+
|
|
119
|
+
## Reference Links
|
|
120
|
+
|
|
121
|
+
- [Odoo Mail Tracking (mail.thread)](https://www.odoo.com/documentation/19.0/developer/reference/backend/orm.html#odoo.models.Model)
|
|
122
|
+
- [Dynamics 365 Database Logging (sysdatabaselog)](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/sysadmin/configure-manage-database-log)
|
|
123
|
+
- [SAP Change Document Header (CDHDR)](https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/f76052846e404ea8811eb06534b6e099/5e13e82b8ca111d1a6090000e8353423.html)
|
|
124
|
+
- [Oracle Financials Audit Trail](https://docs.oracle.com/en/cloud/saas/financials/24d/faigl/overview-of-financials.html)
|
|
125
|
+
- [AWS CloudTrail Documentation](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html)
|
|
126
|
+
- [AWS CloudTrail userIdentity](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Audit Policy Configuration
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Audit Policy Configuration allows administrators to define precisely which entities, fields, and operation types are tracked by the audit system. Rather than auditing everything indiscriminately — which carries significant performance costs — policies let organizations target the data changes that matter most for compliance, security, and operational oversight. Each policy specifies a target entity, a single operation type (CREATE, UPDATE, or DELETE) to record, the subset of fields to capture, and how sensitive field values should be handled during capture. To audit multiple operation types on the same entity, administrators create separate policies — one per operation type.
|
|
6
|
+
|
|
7
|
+
Policies follow a lifecycle state machine (DRAFT → ACTIVE ↔ INACTIVE) that enables safe rollout and rollback of audit configurations. A policy in DRAFT can be reviewed and refined before it takes effect; activating a policy begins audit capture; deactivating a policy stops capture without losing the policy definition or any previously recorded audit entries. Policies can be scoped globally or to a specific company, supporting multi-company deployments where different legal entities have different compliance requirements.
|
|
8
|
+
|
|
9
|
+
A uniqueness constraint ensures that at most one ACTIVE policy exists for a given combination of (entityName, companyId, operationType). This eliminates ambiguity about which field set applies and avoids unintended field-set merges. To change the audited fields for an existing active configuration, use the atomic replacement operation (`replaceAuditPolicy`), which deactivates the current ACTIVE policy and activates the replacement DRAFT policy in a single transaction — guaranteeing no gap in audit coverage. Alternatively, an administrator can manually deactivate the current policy and then activate the replacement, but this two-step approach creates a brief window where no policy is active and events for that entity + operation combination will be silently discarded.
|
|
10
|
+
|
|
11
|
+
Policy scope must match the target entity's scope to eliminate precedence ambiguity. Company-bound entities (entities that always carry a companyId) can only have company-scoped policies — global policies (companyId = null) are rejected at creation time for company-bound entities. Conversely, global entities (entities that never carry a companyId, e.g., user accounts, currencies) can only have global policies — company-scoped policies are rejected at creation time for global entities. This scope-matching rule ensures that for any given operation, exactly zero or one ACTIVE policy can match. Entity scope (company-bound vs global) is declared at entity registration time and is immutable. Because entityName and companyId are immutable after creation, the audit module validates this scope-matching constraint at policy creation time only.
|
|
12
|
+
|
|
13
|
+
## Business Purpose
|
|
14
|
+
|
|
15
|
+
- Regulatory compliance demands that certain fields (e.g., financial amounts, approval statuses) are tracked, while auditing every field is unnecessary and expensive
|
|
16
|
+
- Security teams need visibility into sensitive data changes (e.g., user role assignments, payment details) without flooding the audit log with noise
|
|
17
|
+
- Sensitive data protection requires that field values containing personal data, credentials, or payment details are masked, hashed, or excluded from audit logs to prevent the audit trail from becoming a secondary store of sensitive information
|
|
18
|
+
- Performance optimization requires granular control — Dynamics 365, SAP, and other major ERPs explicitly warn that broad audit configurations degrade system throughput
|
|
19
|
+
- Multi-company organizations need different audit policies per legal entity to reflect varying local regulatory requirements
|
|
20
|
+
- Safe rollout of audit changes is critical — a misconfigured policy should be testable in DRAFT before it starts capturing production data
|
|
21
|
+
- Rollback capability allows administrators to deactivate a policy immediately if it causes performance issues or captures unintended data
|
|
22
|
+
|
|
23
|
+
## Process Flow
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
stateDiagram-v2
|
|
27
|
+
[*] --> Draft: createAuditPolicy
|
|
28
|
+
Draft --> Active: activateAuditPolicy
|
|
29
|
+
Draft --> Active: replaceAuditPolicy (atomic swap:\ndeactivates current ACTIVE,\nactivates this DRAFT)
|
|
30
|
+
Active --> Inactive: deactivateAuditPolicy
|
|
31
|
+
Inactive --> Active: reactivateAuditPolicy
|
|
32
|
+
Draft --> [*]: deleteAuditPolicy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The policy creation and configuration flow follows this pattern:
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
flowchart TD
|
|
39
|
+
A[Create Audit Policy] --> B[Specify Target Entity]
|
|
40
|
+
B --> C[Select Operation Type]
|
|
41
|
+
C --> D[Add Field-Level Rules]
|
|
42
|
+
D --> D2[Set Sensitivity Mode per Field]
|
|
43
|
+
D2 --> E{All fields configured?}
|
|
44
|
+
E -->|No| D
|
|
45
|
+
E -->|Yes| F{Ready to activate?}
|
|
46
|
+
F -->|No| G[Continue editing]
|
|
47
|
+
G --> B
|
|
48
|
+
F -->|Yes| H{Conflicting ACTIVE policy exists?}
|
|
49
|
+
H -->|Yes| H2{Use atomic replacement?}
|
|
50
|
+
H2 -->|Yes| H3[replaceAuditPolicy:\nDeactivate existing + Activate new\nin single transaction]
|
|
51
|
+
H2 -->|No| H4[Reject: deactivate existing first]
|
|
52
|
+
H3 --> J[Audit Capture Begins]
|
|
53
|
+
H -->|No| I[Activate Policy]
|
|
54
|
+
I --> J
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
When a data operation occurs, the system evaluates active policies to determine whether to record an audit entry. Because policy scope must match entity scope, the lookup is unambiguous — there is at most one matching ACTIVE policy:
|
|
58
|
+
|
|
59
|
+
```mermaid
|
|
60
|
+
flowchart TD
|
|
61
|
+
A[Data Operation Occurs] --> B{Entity is company-bound?}
|
|
62
|
+
B -->|Yes| C[Look up ACTIVE policy for entity + companyId + operation]
|
|
63
|
+
B -->|No| D[Look up ACTIVE global policy for entity + operation]
|
|
64
|
+
C --> E{Policy found?}
|
|
65
|
+
D --> E
|
|
66
|
+
E -->|No| F[No audit entry]
|
|
67
|
+
E -->|Yes| G{Field-level rules defined?}
|
|
68
|
+
G -->|No| H[Audit all eligible fields on entity]
|
|
69
|
+
G -->|Yes| I[Audit only specified fields]
|
|
70
|
+
H --> J[Apply sensitivity modes to values]
|
|
71
|
+
I --> J
|
|
72
|
+
J --> K[Record Audit Entry with processed values]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Scenario Patterns
|
|
76
|
+
|
|
77
|
+
- **Compliance-Driven Field Tracking**: A financial services company creates two policies — one for UPDATE and one for DELETE — on the Journal Entry entity, each tracking only the amount, account, and approval status fields. This satisfies SOX requirements without capturing every minor metadata change
|
|
78
|
+
- **Security Monitoring**: An administrator creates three policies (one per operation type: CREATE, UPDATE, DELETE) on the User Role Assignment entity, each capturing every field. This provides a complete record of privilege changes for security review
|
|
79
|
+
- **Sensitive Data Protection**: A policy auditing the Employee entity marks the socialSecurityNumber field with sensitivity mode EXCLUDE (omitted from audit logs entirely) and the salary field with MASK (stored as a masked value like "***"). This ensures the audit trail records that these fields changed without exposing the raw values
|
|
80
|
+
- **Gradual Rollout**: A new audit policy for the Sales Order entity is created in DRAFT and reviewed by the compliance team. After validation, it is activated in a single company first, then additional company-scoped policies are created and activated for other subsidiaries
|
|
81
|
+
- **Performance Remediation**: An overly broad policy auditing all fields on a high-volume entity is deactivated immediately when monitoring reveals degraded throughput. A replacement policy targeting only critical fields is created and activated
|
|
82
|
+
- **Multi-Company Differentiation**: A global organization creates separate policies for its EU and US subsidiaries — the EU policy audits personal data fields to comply with GDPR (with appropriate masking), while the US policy focuses on financial fields for SOX compliance
|
|
83
|
+
- **Atomic Policy Replacement**: An administrator needs to add new fields to an active policy. They create a new DRAFT policy with the updated field set and use `replaceAuditPolicy` to atomically deactivate the existing ACTIVE policy and activate the replacement in a single transaction — ensuring no gap in audit coverage
|
|
84
|
+
- **Manual Policy Replacement (with gap)**: An administrator deactivates the existing ACTIVE policy and then activates a replacement DRAFT policy in two separate steps. During the window between deactivation and activation, events for that entity + operation are silently discarded. This approach is acceptable when a brief audit gap is tolerable
|
|
85
|
+
- **Policy Cleanup**: A DRAFT policy that was created for evaluation but never needed is deleted without affecting any audit data
|
|
86
|
+
|
|
87
|
+
## Test Cases
|
|
88
|
+
|
|
89
|
+
- Audit policy lifecycle follows DRAFT → ACTIVE ↔ INACTIVE state machine
|
|
90
|
+
- Policies can only be created in DRAFT status
|
|
91
|
+
- Only DRAFT policies can be deleted; ACTIVE and INACTIVE policies cannot
|
|
92
|
+
- Target entity name is required and must be non-empty
|
|
93
|
+
- Exactly one operation type (CREATE, UPDATE, or DELETE) must be specified per policy
|
|
94
|
+
- Operation types are limited to the set CREATE, UPDATE, DELETE — invalid values are rejected
|
|
95
|
+
- A policy without field-level rules audits all eligible fields on the target entity (as defined by the entity's auditableFields registration)
|
|
96
|
+
- A policy with field-level rules audits only the specified fields (which must be a subset of the entity's auditableFields)
|
|
97
|
+
- Field names in field-level rules must be non-empty strings
|
|
98
|
+
- Duplicate field names within the same policy are rejected
|
|
99
|
+
- Activating a policy without a target entity is rejected
|
|
100
|
+
- A policy must have exactly one operation type — policies without an operation type cannot be activated
|
|
101
|
+
- Only ACTIVE policies trigger audit capture; DRAFT and INACTIVE policies do not
|
|
102
|
+
- Deactivating a policy stops new audit capture but does not delete previously recorded audit entries
|
|
103
|
+
- Reactivating an INACTIVE policy resumes audit capture
|
|
104
|
+
- Company-scoped policies apply only to operations within that company
|
|
105
|
+
- A global policy (no company scope) applies to operations on global entities
|
|
106
|
+
- At most one ACTIVE policy can exist for a given (entityName, companyId, operationType) combination — activating a conflicting policy is rejected
|
|
107
|
+
- Activating a policy that would conflict with an existing ACTIVE policy returns an error instructing the administrator to deactivate the existing policy first
|
|
108
|
+
- Creating a global policy (companyId = null) for a company-bound entity is rejected
|
|
109
|
+
- Creating a company-scoped policy for a global entity is rejected
|
|
110
|
+
- Entity scope (company-bound vs global) is validated at policy creation time
|
|
111
|
+
- For any given entity + operation, at most one ACTIVE policy can match because policy scope must align with entity scope
|
|
112
|
+
- Deleting an ACTIVE or INACTIVE policy returns an error
|
|
113
|
+
- Each field-level rule supports a sensitivity mode: CAPTURE (default, store raw value), MASK (store masked representation), HASH (store one-way hash), or EXCLUDE (omit value entirely)
|
|
114
|
+
- When sensitivity mode is MASK, the stored value replaces all but the last 4 characters with asterisks (or stores "***" if the value is 4 characters or fewer)
|
|
115
|
+
- When sensitivity mode is HASH, the stored value is a one-way hash of the original value
|
|
116
|
+
- When sensitivity mode is EXCLUDE, the ChangeDetail records that the field changed but stores null for both oldValue and newValue
|
|
117
|
+
- Fields without an explicit sensitivity mode default to CAPTURE (raw value stored)
|
|
118
|
+
- Sensitivity modes are enforced at write time — once an audit entry is persisted, the original raw value cannot be recovered from MASK, HASH, or EXCLUDE entries
|
|
119
|
+
- `replaceAuditPolicy` atomically deactivates the current ACTIVE policy and activates the replacement DRAFT policy in a single transaction
|
|
120
|
+
- `replaceAuditPolicy` requires the replacement policy to be in DRAFT status — non-DRAFT replacements are rejected
|
|
121
|
+
- `replaceAuditPolicy` requires an existing ACTIVE policy for the same (entityName, companyId, operationType) — if none exists, the operation is rejected (use `activateAuditPolicy` instead)
|
|
122
|
+
- After `replaceAuditPolicy`, the previously active policy is in INACTIVE status and the replacement is in ACTIVE status
|
|
123
|
+
- During manual (non-atomic) replacement, events arriving between deactivation and activation are silently discarded
|
|
124
|
+
- Creating, updating, activating, deactivating, reactivating, replacing, and deleting audit policies requires the `manageAuditPolicies` permission at the appropriate scope
|
|
125
|
+
- A caller with company-scoped `manageAuditPolicies` can only manage policies for their assigned companies
|
|
126
|
+
- A caller with global-scoped `manageAuditPolicies` can only manage global policies
|
|
127
|
+
- A caller without `manageAuditPolicies` permission receives an authorization error when attempting policy operations
|
|
128
|
+
|
|
129
|
+
## Reference Links
|
|
130
|
+
|
|
131
|
+
- [Dynamics 365 Configure Entity and Attribute Auditing](https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing)
|
|
132
|
+
- [SAP Change Document Objects](https://help.sap.com/docs/ABAP_PLATFORM_NEW/b5670aaaa2364a29935f40b16499972d/4873464e4cf611d3a6510000e835363f.html)
|
|
133
|
+
- [Odoo Tracking Field Changes](https://www.odoo.com/documentation/19.0/developer/reference/backend/orm.html)
|
|
134
|
+
- [AWS CloudTrail Trail Configuration](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html)
|
|
135
|
+
- [Microsoft Dataverse Auditing](https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Field-Level Change Tracking
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Field-Level Change Tracking records the old and new values for each individual field that changes within a business operation, enabling precise before/after comparison at the most granular level. Each change detail is linked to a parent audit entry and captures the field name, previous value, and new value as serialized strings. This approach follows the common ERP pattern of a ChangeDetail entity associated with a parent AuditEntry by auditEntryId.
|
|
6
|
+
|
|
7
|
+
Not all field types are eligible for audit capture. Scalar fields (string, number, boolean, date, enum) and short text fields (≤ 4,000 characters) are serialized as strings and stored as-is. Long text fields (> 4,000 characters) are truncated to 4,000 characters with a `[truncated]` suffix. Rich text / HTML fields are stripped to plain text, then truncated to 4,000 characters if needed. Binary / BLOB fields are excluded from audit capture entirely and are not eligible for policy field rules. File attachment references capture only the reference metadata (file ID, filename, size), not the file content. Relation fields (foreign keys) capture the foreign key ID as a string. Collection / array fields are serialized as JSON strings and truncated to 4,000 characters if the serialized form exceeds the limit. Computed / derived fields are excluded from audit capture because they are not persisted state. Fields not listed in the entity's `auditableFields` registration are never captured regardless of type. The 4,000-character truncation limit applies after sensitivity mode processing (MASK/HASH/CAPTURE).
|
|
8
|
+
|
|
9
|
+
By storing values as serialized strings regardless of the original data type, the system maintains a uniform storage model while supporting any eligible field type. For CREATE operations the old value is null and the new value contains the initial value; for DELETE operations the old value contains the last known value and the new value is null; for UPDATE operations both values are populated but only for fields that actually changed. This design keeps the audit trail compact while providing full traceability.
|
|
10
|
+
|
|
11
|
+
Field values are subject to the sensitivity mode defined in the applicable audit policy. Fields configured as MASK store a masked representation, HASH fields store a one-way hash, and EXCLUDE fields record that a change occurred but store null for both old and new values. This ensures that change tracking does not become a secondary store of sensitive data.
|
|
12
|
+
|
|
13
|
+
## Business Purpose
|
|
14
|
+
|
|
15
|
+
- **Regulatory compliance**: Auditors and regulators require evidence of exactly what changed, when, and by whom — field-level tracking satisfies SOX, GDPR, and similar mandates that demand detailed change history
|
|
16
|
+
- **Sensitive data protection**: Field values are processed through sensitivity modes (CAPTURE, MASK, HASH, EXCLUDE) defined in audit policies before storage, preventing the audit trail from containing raw personal data, credentials, or payment information
|
|
17
|
+
- **Dispute resolution**: When discrepancies arise (e.g., pricing, quantities, addresses), field-level history provides an authoritative record of the before and after state for each affected field
|
|
18
|
+
- **Operational debugging**: Support teams can quickly identify which field change caused a downstream issue without reviewing entire record snapshots
|
|
19
|
+
- **Rollback analysis**: Knowing the previous value of each changed field enables informed decisions about whether and how to revert a change
|
|
20
|
+
- **Change auditing across modules**: Every transactional module (accounting, sales, purchasing, inventory) and global modules (user-management, primitives) benefit from a single, consistent mechanism for capturing field-level mutations
|
|
21
|
+
|
|
22
|
+
## Process Flow
|
|
23
|
+
|
|
24
|
+
```mermaid
|
|
25
|
+
flowchart TD
|
|
26
|
+
A[Business Operation Occurs] --> B[Determine Operation Type]
|
|
27
|
+
B --> C{CREATE / UPDATE / DELETE?}
|
|
28
|
+
C -->|CREATE| D[Capture all initial field values]
|
|
29
|
+
C -->|UPDATE| E[Compare old and new record states]
|
|
30
|
+
C -->|DELETE| F[Capture all last-known field values]
|
|
31
|
+
D --> G[Generate ChangeDetail entries\noldValue = null, newValue = initial value]
|
|
32
|
+
E --> H[Generate ChangeDetail entries\nonly for fields that differ]
|
|
33
|
+
F --> I[Generate ChangeDetail entries\noldValue = last value, newValue = null]
|
|
34
|
+
G --> J[Apply sensitivity modes from policy]
|
|
35
|
+
H --> J
|
|
36
|
+
I --> J
|
|
37
|
+
J --> K[Link ChangeDetail records to parent AuditEntry]
|
|
38
|
+
K --> L[Persist AuditEntry + ChangeDetails]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```mermaid
|
|
42
|
+
flowchart TD
|
|
43
|
+
A[Query Audit History for Record] --> B[Retrieve AuditEntry list]
|
|
44
|
+
B --> C[Select AuditEntry of interest]
|
|
45
|
+
C --> D[Retrieve linked ChangeDetail records]
|
|
46
|
+
D --> E[Display field-by-field before/after comparison]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Scenario Patterns
|
|
50
|
+
|
|
51
|
+
- **Price Change Audit**: A sales manager updates the unit price on a quotation line. The system records a ChangeDetail with fieldName = unitPrice, oldValue = "100.00", and newValue = "95.00", providing a clear trail for margin analysis and approval workflows
|
|
52
|
+
- **Address Correction**: A customer service representative corrects a shipping address. Each modified address field (street, city, postalCode) produces its own ChangeDetail entry, allowing reviewers to see exactly which parts of the address were changed
|
|
53
|
+
- **Sensitive Field Masking**: An HR administrator updates an employee's salary. The policy defines salary with sensitivity mode MASK, so the ChangeDetail stores oldValue = "***" and newValue = "***", recording that the field changed without exposing the actual values
|
|
54
|
+
- **Excluded Field**: A system updates a user's password hash. The policy defines passwordHash with sensitivity mode EXCLUDE, so the ChangeDetail stores fieldName = "passwordHash" with both oldValue = null and newValue = null, recording that the field changed without storing any value
|
|
55
|
+
- **Record Creation Baseline**: When a new purchase order is created, ChangeDetail entries are generated for every populated field with oldValue = null, establishing the initial baseline for all future change comparisons
|
|
56
|
+
- **Record Deletion Snapshot**: When a draft invoice is deleted, ChangeDetail entries capture the last known value of every field with newValue = null, preserving a complete snapshot of the record at the time of deletion
|
|
57
|
+
- **Bulk Field Update**: An administrator updates the cost center on 50 inventory items. Each item's audit entry contains a single ChangeDetail for the costCenter field, keeping the audit trail compact by recording only the field that changed rather than full record snapshots
|
|
58
|
+
- **No-Op Detection**: A user saves a record without modifying any fields. No ChangeDetail entries are created because no field values differ, avoiding unnecessary audit noise
|
|
59
|
+
|
|
60
|
+
## Test Cases
|
|
61
|
+
|
|
62
|
+
- Each ChangeDetail record is linked to exactly one parent AuditEntry via auditEntryId
|
|
63
|
+
- ChangeDetail captures fieldName, oldValue, and newValue as serialized strings
|
|
64
|
+
- For a CREATE operation, oldValue is null and newValue contains the initial field value
|
|
65
|
+
- For a DELETE operation, oldValue contains the last known value and newValue is null
|
|
66
|
+
- For an UPDATE operation, both oldValue and newValue are populated
|
|
67
|
+
- UPDATE operations generate ChangeDetail entries only for fields whose values actually changed
|
|
68
|
+
- Fields that did not change during an UPDATE are not recorded as ChangeDetail entries
|
|
69
|
+
- A save operation with no field changes produces zero ChangeDetail entries
|
|
70
|
+
- Multiple field changes within a single operation each produce a separate ChangeDetail record under the same AuditEntry
|
|
71
|
+
- fieldName is required and must be non-empty on every ChangeDetail record
|
|
72
|
+
- Values are stored as serialized strings regardless of the original data type (integer, boolean, date, etc.)
|
|
73
|
+
- Querying ChangeDetails by auditEntryId returns all field-level changes for that audit entry
|
|
74
|
+
- ChangeDetail records are immutable once persisted — they cannot be updated or deleted
|
|
75
|
+
- A CREATE operation on a record with N populated fields produces N ChangeDetail entries
|
|
76
|
+
- When the policy sensitivity mode for a field is MASK, oldValue and newValue store masked representations
|
|
77
|
+
- When the policy sensitivity mode for a field is HASH, oldValue and newValue store one-way hashes
|
|
78
|
+
- When the policy sensitivity mode for a field is EXCLUDE, both oldValue and newValue are null, but the ChangeDetail record is still created to indicate the field changed
|
|
79
|
+
- Fields without an explicit sensitivity mode in the policy default to CAPTURE (raw values stored)
|
|
80
|
+
- Binary / BLOB fields are excluded from audit capture and cannot be added to policy field rules
|
|
81
|
+
- Computed / derived fields are excluded from audit capture
|
|
82
|
+
- Long text values exceeding 4,000 characters are truncated with a `[truncated]` suffix
|
|
83
|
+
- Rich text / HTML field values are stripped to plain text before storage
|
|
84
|
+
- Collection / array field values are serialized as JSON strings
|
|
85
|
+
- Serialized values exceeding 4,000 characters are truncated with a `[truncated]` suffix
|
|
86
|
+
- Relation field values capture the foreign key ID as a string
|
|
87
|
+
- File attachment fields capture the reference metadata (file ID, filename, size), not the file content
|
|
88
|
+
- Truncation is applied after sensitivity mode processing
|
|
89
|
+
|
|
90
|
+
## Reference Links
|
|
91
|
+
|
|
92
|
+
- [SAP Change Documents (CDHDR/CDPOS)](https://help.sap.com/doc/saphelp_nw75/7.5.5/en-US/4d/b2ba6fbb571e57e10000000a42189b/frameset.htm)
|
|
93
|
+
- [Odoo Mail Tracking Values](https://www.odoo.com/documentation/19.0/developer/reference/backend/logging.html)
|
|
94
|
+
- [Dynamics 365 Auditing Overview](https://learn.microsoft.com/en-us/power-platform/admin/manage-dataverse-auditing)
|
|
95
|
+
- [AWS CloudTrail Log Event Reference](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference.html)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# AuditEntry
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
AuditEntry represents a single immutable record of a data mutation captured by the audit system. Each entry records a unique `eventId` for deduplication and idempotency, the actor identity (actorType and actorId, with optional metadata such as IP address, user agent, sessionId, requestId, or onBehalfOf reference), the operation type (CREATE, UPDATE, DELETE), the affected entity reference (entityType and entityId), and a system-generated timestamp. Entries may be scoped by companyId for multi-company data isolation; entries for global entities omit companyId. An optional correlationId groups related events within a logical operation (e.g., bulk updates, cascading workflows).
|
|
6
|
+
|
|
7
|
+
Examples: "User jdoe updated SalesOrder SO-1234 at 2025-01-15T10:30:00Z", "System job recalculated exchange rates (global scope, correlationId groups all rate changes)".
|
|
8
|
+
|
|
9
|
+
## Domain Model Definitions
|
|
10
|
+
|
|
11
|
+
### Model type
|
|
12
|
+
|
|
13
|
+
AppendOnly
|
|
14
|
+
|
|
15
|
+
### Command Definitions
|
|
16
|
+
|
|
17
|
+
- [logAuditEvent](../commands/LogAuditEvent.md) - Ingest an audit event, creating an AuditEntry with linked ChangeDetail records
|
|
18
|
+
|
|
19
|
+
### Query Definitions
|
|
20
|
+
|
|
21
|
+
- [getAuditEntry](../queries/GetAuditEntry.md) - Retrieve a single audit entry by ID
|
|
22
|
+
- [searchAuditEntries](../queries/SearchAuditEntries.md) - Search and filter audit entries by actor, entity, operation type, date range, and company scope
|
|
23
|
+
- [getAuditSummary](../queries/GetAuditSummary.md) - Retrieve aggregated audit entry counts grouped by configurable dimensions
|
|
24
|
+
|
|
25
|
+
### Models
|
|
26
|
+
|
|
27
|
+
- AuditEntry
|
|
28
|
+
- ChangeDetail
|
|
29
|
+
|
|
30
|
+
### Invariants
|
|
31
|
+
|
|
32
|
+
- eventId is globally unique (UUID) and used for idempotency — duplicate eventId submissions are silently discarded (returns success, no new entry created)
|
|
33
|
+
- Once persisted, an AuditEntry cannot be modified or deleted (append-only, immutable)
|
|
34
|
+
- actorType is required and must be one of USER, SYSTEM, or SERVICE
|
|
35
|
+
- actorId is required and must be non-empty regardless of actorType
|
|
36
|
+
- An operation submitted without a valid actor identity (actorType + actorId) is rejected
|
|
37
|
+
- entityType must reference a registered auditable entity — events referencing an unregistered entityType are rejected with a validation error
|
|
38
|
+
- operationType must be one of CREATE, UPDATE, or DELETE
|
|
39
|
+
- companyId is required for company-bound entities and must be null for global entities — scope mismatches are rejected with a validation error
|
|
40
|
+
- timestamp is system-generated at ingestion time and cannot be supplied or overridden by the caller
|
|
41
|
+
- correlationId is optional (UUID) and groups related events from a single logical operation (e.g., bulk updates, cascading workflows, saga steps)
|
|
42
|
+
- onBehalfOf is optional and stores the delegated user's identity when an action is performed on behalf of another actor
|
|
43
|
+
- actorMetadata (ipAddress, userAgent, sessionId, requestId) is optional and stored when provided
|
|
44
|
+
- An event for an entity + operation combination with no active policy is silently discarded (no entry created)
|
|
45
|
+
- Audit entries are returned in chronological order by default
|
|
46
|
+
- Large result sets support pagination for efficient retrieval
|
|
47
|
+
- Querying audit entries requires the `viewAuditHistory` permission at the appropriate scope
|
|
48
|
+
- Querying audit summaries requires the `viewAuditSummary` permission at the appropriate scope
|
|
49
|
+
|
|
50
|
+
### Relationships
|
|
51
|
+
|
|
52
|
+
- **Has Many ChangeDetail**: Each AuditEntry has zero or more ChangeDetail records linked by auditEntryId, capturing field-level changes
|
|
53
|
+
- **References AuditableEntity**: entityType must match a registered AuditableEntity's entityName
|
|
54
|
+
- **References user-management (cross-module)**: actorId references a userId when actorType is USER; onBehalfOf references a delegated userId
|
|
55
|
+
- **References organization (cross-module)**: companyId references a Company from the organization module for company-scoped entries
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# AuditPolicy
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
AuditPolicy defines which entities, operations, and fields are tracked by the audit system. Each policy targets a specific entity and operation type, with an optional company scope for multi-company deployments. Policies follow a lifecycle state machine (DRAFT → ACTIVE ↔ INACTIVE) that enables safe rollout and rollback. Only ACTIVE policies trigger audit capture. A uniqueness constraint ensures at most one ACTIVE policy exists for a given (entityName, companyId, operationType) combination. Policy scope must match the target entity's scope — company-bound entities require company-scoped policies; global entities require global policies.
|
|
6
|
+
|
|
7
|
+
Examples: "ACTIVE policy auditing UPDATE on SalesOrder for company-123", "DRAFT policy under review for DELETE on JournalEntry".
|
|
8
|
+
|
|
9
|
+
## Domain Model Definitions
|
|
10
|
+
|
|
11
|
+
### Model type
|
|
12
|
+
|
|
13
|
+
Stateful
|
|
14
|
+
|
|
15
|
+
#### State Transitions
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
stateDiagram-v2
|
|
19
|
+
[*] --> Draft: createAuditPolicy
|
|
20
|
+
Draft --> Active: activateAuditPolicy
|
|
21
|
+
Draft --> Active: replaceAuditPolicy (atomic swap)
|
|
22
|
+
Active --> Inactive: deactivateAuditPolicy
|
|
23
|
+
Inactive --> Active: reactivateAuditPolicy
|
|
24
|
+
Draft --> [*]: deleteAuditPolicy
|
|
25
|
+
note right of Draft: Only DRAFT policies can be deleted
|
|
26
|
+
note right of Active: At most one ACTIVE per (entityName, companyId, operationType)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Command Definitions
|
|
30
|
+
|
|
31
|
+
- [createAuditPolicy](../commands/CreateAuditPolicy.md) - Create a new audit policy in DRAFT status
|
|
32
|
+
- [updateAuditPolicy](../commands/UpdateAuditPolicy.md) - Modify a DRAFT audit policy's configuration
|
|
33
|
+
- [activateAuditPolicy](../commands/ActivateAuditPolicy.md) - Transition a DRAFT policy to ACTIVE
|
|
34
|
+
- [deactivateAuditPolicy](../commands/DeactivateAuditPolicy.md) - Transition an ACTIVE policy to INACTIVE
|
|
35
|
+
- [reactivateAuditPolicy](../commands/ReactivateAuditPolicy.md) - Transition an INACTIVE policy back to ACTIVE
|
|
36
|
+
- [replaceAuditPolicy](../commands/ReplaceAuditPolicy.md) - Atomically deactivate current ACTIVE and activate a DRAFT replacement
|
|
37
|
+
- [deleteAuditPolicy](../commands/DeleteAuditPolicy.md) - Permanently remove a DRAFT policy
|
|
38
|
+
|
|
39
|
+
### Query Definitions
|
|
40
|
+
|
|
41
|
+
- [getAuditPolicy](../queries/GetAuditPolicy.md) - Retrieve a single audit policy by ID
|
|
42
|
+
- [listAuditPolicies](../queries/ListAuditPolicies.md) - List audit policies with optional filters
|
|
43
|
+
|
|
44
|
+
### Models
|
|
45
|
+
|
|
46
|
+
- AuditPolicy
|
|
47
|
+
- PolicyFieldRule
|
|
48
|
+
|
|
49
|
+
### Invariants
|
|
50
|
+
|
|
51
|
+
- Policies are always created in DRAFT status
|
|
52
|
+
- At most one ACTIVE policy can exist for a given (entityName, companyId, operationType) combination — activating a conflicting policy is rejected with an error instructing the administrator to deactivate the existing policy first
|
|
53
|
+
- Only DRAFT policies can be deleted; ACTIVE and INACTIVE policies cannot — deleting an ACTIVE or INACTIVE policy returns an error
|
|
54
|
+
- Target entity name is required and must be non-empty
|
|
55
|
+
- Target entity name must reference a registered AuditableEntity
|
|
56
|
+
- Exactly one operation type (CREATE, UPDATE, or DELETE) must be specified per policy — policies without an operation type cannot be activated
|
|
57
|
+
- Operation types are limited to the set CREATE, UPDATE, DELETE — invalid values are rejected
|
|
58
|
+
- Activating a policy without a target entity is rejected
|
|
59
|
+
- Policy scope must match the target entity's scope: company-bound entities require company-scoped policies (companyId set); global entities require global policies (companyId null) — mismatches are rejected at creation time
|
|
60
|
+
- entityName and companyId are immutable after creation — scope is validated only at creation time; updates cannot change the policy's scope
|
|
61
|
+
- Only ACTIVE policies trigger audit capture; DRAFT and INACTIVE policies do not
|
|
62
|
+
- Deactivating a policy stops new audit capture but does not delete previously recorded audit entries
|
|
63
|
+
- Reactivating an INACTIVE policy resumes audit capture
|
|
64
|
+
- A policy without field-level rules audits all eligible fields on the target entity (as defined by the entity's auditableFields registration)
|
|
65
|
+
- A policy with field-level rules audits only the specified fields (which must be a subset of the entity's auditableFields)
|
|
66
|
+
- `replaceAuditPolicy` atomically deactivates the current ACTIVE policy and activates the replacement DRAFT policy in a single transaction
|
|
67
|
+
- `replaceAuditPolicy` requires the replacement policy to be in DRAFT status — non-DRAFT replacements are rejected
|
|
68
|
+
- `replaceAuditPolicy` requires an existing ACTIVE policy for the same (entityName, companyId, operationType) — if none exists, the operation is rejected (use `activateAuditPolicy` instead)
|
|
69
|
+
- After `replaceAuditPolicy`, the previously active policy is in INACTIVE status and the replacement is in ACTIVE status
|
|
70
|
+
- During manual (non-atomic) replacement, events arriving between deactivation and activation are silently discarded
|
|
71
|
+
- Creating, updating, activating, deactivating, reactivating, replacing, and deleting audit policies requires the `manageAuditPolicies` permission at the appropriate scope
|
|
72
|
+
- A caller with company-scoped `manageAuditPolicies` can only manage policies for their assigned companies
|
|
73
|
+
- A caller with global-scoped `manageAuditPolicies` can only manage global policies
|
|
74
|
+
|
|
75
|
+
### Relationships
|
|
76
|
+
|
|
77
|
+
- **Has Many PolicyFieldRule**: Each AuditPolicy has zero or more PolicyFieldRule records defining field-level audit configuration
|
|
78
|
+
- **References AuditableEntity**: entityName must match a registered AuditableEntity
|
|
79
|
+
- **References organization (cross-module)**: companyId references a Company from the organization module for company-scoped policies
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# AuditableEntity
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
AuditableEntity represents a registration record for an entity that is eligible for audit tracking. Other modules register their entities at initialization time by declaring the entityName, entityScope (COMPANY_BOUND or GLOBAL), and the list of auditableFields eligible for capture. The audit module validates incoming audit events against registered entities — events referencing an unregistered entityType are rejected. Entity scope is immutable once registered and determines whether policies and audit entries for this entity must be company-scoped or global.
|
|
6
|
+
|
|
7
|
+
Examples: "SalesOrder registered as COMPANY_BOUND with fields [{fieldName: 'amount', fieldType: 'scalar'}, {fieldName: 'status', fieldType: 'scalar'}, {fieldName: 'customerId', fieldType: 'relation'}]", "User registered as GLOBAL with fields [{fieldName: 'email', fieldType: 'scalar'}, {fieldName: 'role', fieldType: 'scalar'}, {fieldName: 'isActive', fieldType: 'scalar'}]".
|
|
8
|
+
|
|
9
|
+
## Domain Model Definitions
|
|
10
|
+
|
|
11
|
+
### Model type
|
|
12
|
+
|
|
13
|
+
Standard
|
|
14
|
+
|
|
15
|
+
### Command Definitions
|
|
16
|
+
|
|
17
|
+
- [registerAuditableEntity](../commands/RegisterAuditableEntity.md) - Register a new entity for audit tracking
|
|
18
|
+
|
|
19
|
+
### Query Definitions
|
|
20
|
+
|
|
21
|
+
None. AuditableEntity registrations are referenced internally by the audit module during policy creation and event ingestion for validation purposes.
|
|
22
|
+
|
|
23
|
+
### Models
|
|
24
|
+
|
|
25
|
+
- AuditableEntity
|
|
26
|
+
|
|
27
|
+
### Invariants
|
|
28
|
+
|
|
29
|
+
- entityName is required, must be non-empty, and must be unique across all registrations
|
|
30
|
+
- entityScope must be one of COMPANY_BOUND or GLOBAL and is immutable once registered
|
|
31
|
+
- auditableFields is a JSON-serialized array of field definitions (`{fieldName: string, fieldType?: string}`) eligible for audit capture; must contain at least one entry
|
|
32
|
+
- Only the following fieldType values are eligible for inclusion in auditableFields: `scalar`, `richtext`, `array`, `relation`, `file`. Any other fieldType (e.g. `binary`, `computed`) is rejected
|
|
33
|
+
|
|
34
|
+
### Relationships
|
|
35
|
+
|
|
36
|
+
- **Referenced By AuditEntry**: AuditEntry.entityType must match a registered AuditableEntity.entityName
|
|
37
|
+
- **Referenced By AuditPolicy**: AuditPolicy.entityName must match a registered AuditableEntity.entityName
|
|
38
|
+
- **Referenced By emitting modules (reverse dependency)**: Other ERP modules register their auditable entities at initialization time
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ChangeDetail
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
ChangeDetail records the old and new values for a single field that changed within a business operation. Each ChangeDetail is linked to a parent AuditEntry via auditEntryId and captures the fieldName, previous value (oldValue), and new value (newValue) as serialized strings. Values are subject to the sensitivity mode defined in the applicable audit policy: CAPTURE stores raw values, MASK stores masked representations, HASH stores one-way hashes, and EXCLUDE stores null for both values while still recording that the field changed. Values exceeding 4,000 characters are truncated with a `[truncated]` suffix after sensitivity mode processing.
|
|
6
|
+
|
|
7
|
+
Examples: "unitPrice changed from '100.00' to '95.00'", "salary changed (MASK mode: '***' to '***')".
|
|
8
|
+
|
|
9
|
+
## Domain Model Definitions
|
|
10
|
+
|
|
11
|
+
### Model type
|
|
12
|
+
|
|
13
|
+
AppendOnly
|
|
14
|
+
|
|
15
|
+
### Command Definitions
|
|
16
|
+
|
|
17
|
+
- [logAuditEvent](../commands/LogAuditEvent.md) - ChangeDetail records are created as part of the logAuditEvent command, linked to the parent AuditEntry
|
|
18
|
+
|
|
19
|
+
### Query Definitions
|
|
20
|
+
|
|
21
|
+
- [getChangeDetails](../queries/GetChangeDetails.md) - Retrieve all field-level change details for a given audit entry
|
|
22
|
+
|
|
23
|
+
### Models
|
|
24
|
+
|
|
25
|
+
- ChangeDetail
|
|
26
|
+
|
|
27
|
+
### Invariants
|
|
28
|
+
|
|
29
|
+
- Each ChangeDetail belongs to exactly one parent AuditEntry via auditEntryId
|
|
30
|
+
- fieldName is required and must be non-empty
|
|
31
|
+
- Values are stored as serialized strings regardless of original data type (integer, boolean, date, etc.)
|
|
32
|
+
- For CREATE operations, oldValue is null and newValue contains the initial value
|
|
33
|
+
- For DELETE operations, oldValue contains the last known value and newValue is null
|
|
34
|
+
- For UPDATE operations, both oldValue and newValue are populated, but only for fields that actually changed
|
|
35
|
+
- A save operation with no field changes produces zero ChangeDetail entries (no-op detection)
|
|
36
|
+
- A CREATE operation on a record with N populated fields produces N ChangeDetail entries
|
|
37
|
+
- Once persisted, ChangeDetail records cannot be modified or deleted (immutable)
|
|
38
|
+
- Binary / BLOB fields are excluded from capture entirely and are not eligible for policy field rules
|
|
39
|
+
- Computed / derived fields are excluded from capture
|
|
40
|
+
- Long text fields (> 4,000 characters) are truncated to 4,000 characters with a `[truncated]` suffix
|
|
41
|
+
- Rich text / HTML field values are stripped to plain text before storage, then truncated if needed
|
|
42
|
+
- Collection / array field values are serialized as JSON strings and truncated to 4,000 characters if the serialized form exceeds the limit
|
|
43
|
+
- Relation field values capture the foreign key ID as a string
|
|
44
|
+
- File attachment fields capture reference metadata (file ID, filename, size), not the file content
|
|
45
|
+
- Truncation is applied after sensitivity mode processing (MASK/HASH/CAPTURE)
|
|
46
|
+
- When sensitivity mode is CAPTURE (default), raw values are stored
|
|
47
|
+
- When sensitivity mode is MASK, oldValue and newValue store masked representations
|
|
48
|
+
- When sensitivity mode is HASH, oldValue and newValue store one-way hashes
|
|
49
|
+
- When sensitivity mode is EXCLUDE, both oldValue and newValue are null, but the ChangeDetail record is still created to indicate the field changed
|
|
50
|
+
- Fields without an explicit sensitivity mode in the policy default to CAPTURE
|
|
51
|
+
|
|
52
|
+
### Relationships
|
|
53
|
+
|
|
54
|
+
- **Belongs To AuditEntry**: Each ChangeDetail is linked to exactly one AuditEntry via auditEntryId
|
|
55
|
+
- **Governed By PolicyFieldRule**: Sensitivity mode applied to stored values is determined by the PolicyFieldRule associated with the parent AuditEntry's applicable AuditPolicy
|