Appearance
Journal Entry Complete Walkthrough — Example-Based Documentation
Overview
This document demonstrates the complete lifecycle of a journal entry using real payload examples, showing exactly which tables and columns are affected during Add (Draft), Post, Reversal, and other operations.
Journal entries in the ERP system are the foundation of the General Ledger. They can be manual (created directly by users) or internal (system-generated from transactions like Sales Invoices, Payments, Stock movements).
Example Payload — Manual Journal Entry
json
{
"refrenceNumber": "JV-001",
"journalDate": "2026-01-28T16:14:36",
"type": "Manual",
"description": "Monthly rent expense recognition",
"periodId": "FY-2026-Jan",
"isHeaderDescriptionCopied": true,
"journalEntryLines": [
{
"accountId": 5020,
"lineDescription": "Monthly rent expense recognition",
"debitAmount": 5000.00,
"creditAmount": 0.00,
"currencyId": 4,
"currencyRate": 1.0,
"isVatLine": false,
"hasVat": false,
"costCenters": [
{ "costCenterId": 87, "percentage": 100 }
]
},
{
"accountId": 1010,
"lineDescription": "Monthly rent expense recognition",
"debitAmount": 0.00,
"creditAmount": 5000.00,
"currencyId": 4,
"currencyRate": 1.0,
"isVatLine": false,
"hasVat": false,
"costCenters": []
}
],
"journalEntryAttachments": [
{ "attachmentId": "abc123", "name": "rent-invoice.pdf" }
]
}Pre-Save Calculations
Line-Level:
DebitAmountLocal = DebitAmount × CurrencyRate = 5000 × 1.0 = 5000
CreditAmountLocal = CreditAmount × CurrencyRate = 5000 × 1.0 = 5000
Header Totals:
TotalDebitAmount = Sum of all line DebitAmounts = 5000
TotalCreditAmount = Sum of all line CreditAmounts = 5000
TotalDebitAmountLocal = Sum of all line DebitAmountLocal = 5000
TotalCreditAmountLocal = Sum of all line CreditAmountLocal = 5000
Balance Check:
IsBalanced = TotalDebitAmountLocal == TotalCreditAmountLocal → truePhase 1: Add Journal Entry (Draft)
Handler: AddJournalEntryCommandHandler
Action: Creates a draft journal entry with no GL impact until posted.
Validations Performed
| Check | Rule | Result |
|---|---|---|
| Reference number length | Max 15 characters | ✅ "JV-001" = 6 chars |
| Journal date required | Not null/empty | ✅ Provided |
| Description required | Not null/empty | ✅ Provided |
| Open financial period | Period resolved from JournalDate via GetCurrentPeriodQuery must be open | ✅ Jan 2026 period open |
| Line debit/credit exclusive | Each line: debit XOR credit (not both non-zero) | ✅ Mutually exclusive |
| Currency rate required | Each line must have currencyRate > 0 | ✅ 1.0 provided |
| Cost center sum | If account requires cost centers, percentages must sum to 100% | ✅ Line 1: 100% |
| Line description (optional) | Configurable in GeneralSettings | ✅ Copied from header |
Table 1: JournalEntry (INSERT)
Operation: INSERT new record
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., 3fa85f64-5717-4562-b3fc-2c963f66afa6 |
| Code | "JE-2026-0001" | Sequence Service | Generated by UpdateLastSequenceCommand |
| RefrenceNumber | "JV-001" | Payload | User-supplied reference (note: intentional typo in column name) |
| JournalDate | 2026-01-28 16:14:36 | Payload | Transaction date |
| Type | "Manual" | Payload | JournalEntryType enum |
| Description | "Monthly rent expense recognition" | Payload | Journal description |
| Status | "Draft" | SetAsDraft() | TransactionStatus enum |
| FinancialYearPeriodId | [Integer DB ID, e.g. 25] | GetCurrentPeriodQuery(JournalDate) | Resolved from the journal date — command.PeriodId (string) is not used by the handler |
| TotalDebitAmount | 5000.00 | Calculated | Sum of line debits (foreign currency) |
| TotalCreditAmount | 5000.00 | Calculated | Sum of line credits (foreign currency) |
| TotalDebitAmountLocal | 5000.00 | Calculated | Sum of line debits × rate (base currency) |
| TotalCreditAmountLocal | 5000.00 | Calculated | Sum of line credits × rate (base currency) |
| SourceDocument | NULL | Not system-generated | Set only for internal journals |
| SourceDocumentId | NULL | Not system-generated | Reference to source transaction |
| SourceDocumentCode | NULL | Not system-generated | Source transaction code |
| IsReversed | false | Default | Not yet reversed |
| ReversedJournalId | NULL | Default | No reversal yet |
| ReversedFromJournalId | NULL | Default | Not a reversal |
| IsHeaderDescriptionCopied | true | Payload | Description propagated to all lines |
| WorkflowStatus | NULL | Default | Set if workflow enabled |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| CreatedBy | [Current User ID] | ISecurityService | Authenticated user |
| TenantId | [Tenant GUID] | ISecurityService | Multi-tenancy |
| IsDeleted | false | Default | Soft delete flag |
Table 2: JournalEntryLine (INSERT × 2)
Operation: INSERT one record per line
Line 1 — Rent Expense (Debit)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Line 1 ID |
| JournalEntryId | [Journal GUID above] | FK | Parent journal |
| AccountId | 5020 | Payload | Rent expense GL account |
| AccountCode | "5020" | Account master | Account code |
| AccountName | "Rent Expense" | Account master | Account name |
| AccountNameAr | "مصروف الإيجار" | Account master | Arabic account name |
| LineDescription | "Monthly rent expense recognition" | Payload / Header copy | Copied from header |
| DebitAmount | 5000.00 | Payload | In foreign currency |
| CreditAmount | 0.00 | Payload | No credit on this line |
| DebitAmountLocal | 5000.00 | DebitAmount × CurrencyRate | Base currency |
| CreditAmountLocal | 0.00 | CreditAmount × CurrencyRate | Base currency |
| CurrencyId | 4 | Payload | Saudi Riyal |
| CurrencyRate | 1.0 | Payload | Exchange rate to base currency |
| IsVatLine | false | Payload | Not a VAT line |
| HasVat | false | Payload | No VAT on this line |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| CreatedBy | [Current User ID] | ISecurityService | Authenticated user |
| TenantId | [Tenant GUID] | ISecurityService | Multi-tenancy |
Line 2 — Accounts Payable (Credit)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Line 2 ID |
| JournalEntryId | [Journal GUID above] | FK | Parent journal |
| AccountId | 1010 | Payload | Payable/bank GL account |
| LineDescription | "Monthly rent expense recognition" | Payload / Header copy | Copied from header |
| DebitAmount | 0.00 | Payload | No debit on this line |
| CreditAmount | 5000.00 | Payload | In foreign currency |
| DebitAmountLocal | 0.00 | Calculated | Base currency |
| CreditAmountLocal | 5000.00 | Calculated | Base currency |
| CurrencyId | 4 | Payload | Saudi Riyal |
| CurrencyRate | 1.0 | Payload | Exchange rate |
| IsVatLine | false | Payload | Not a VAT line |
| HasVat | false | Payload | No VAT |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
Table 3: JournalLineCostCenter (INSERT)
Operation: INSERT — one record per cost center allocation per line
Line 1 has one cost center allocation at 100%:
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Allocation ID |
| JournalEntryLineId | [Line 1 GUID] | FK | Parent line |
| CostCenterId | 87 | Payload | Cost center ID |
| Percentage | 100.0 | Payload | 100% allocation |
| Amount | 5000.00 | Calculated | Line.DebitAmountLocal × Percentage/100 |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
Line 2 has no cost center (empty array) → no inserts.
Table 4: JournalEntryAttachment (INSERT)
Operation: INSERT — one record per attachment
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Attachment link ID |
| JournalEntryId | [Journal GUID] | FK | Parent journal |
| AttachmentId | "abc123" | Payload | ID in Attachment microservice |
| Name | "rent-invoice.pdf" | Payload | Display name |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
Summary of Add Phase (Draft)
Tables Affected:
- ✅ JournalEntry — 1 INSERT
- ✅ JournalEntryLine — 2 INSERTs
- ✅ JournalLineCostCenter — 1 INSERT (Line 1 only)
- ✅ JournalEntryAttachment — 1 INSERT
- ❌ General Ledger balances — NO update (draft only)
GL Impact: NONE — no financial effect until posted.
Phase 2: Post Journal Entry
Handler: ChangeStatusJournalEntriesCommandHandler (batch) or ChangeStatusJournalEntryCommandHandler (single)
Action: Marks the entry as Posted. This finalizes the journal entry and makes it part of the official General Ledger.
Prerequisites Check
Before posting, system validates:
| Check | Rule |
|---|---|
| Must be Manual type | SourceDocument is NULL |
| Must be in Draft status | Status == Draft |
| Must be balanced | TotalDebitAmountLocal == TotalCreditAmountLocal |
Table 1: JournalEntry (UPDATE)
Operation: UPDATE existing record
| Column Name | Before (Draft) | After (Posted) | Notes |
|---|---|---|---|
| Status | "Draft" | "Posted" | TransactionStatus enum |
| UpdatedDate | NULL | [Current Timestamp] | Last update timestamp |
| UpdatedBy | NULL | [Current User ID] | Updated by user |
All other columns remain unchanged.
GL Impact After Posting
Journal JE-2026-0001 — Rent Expense
DR 5020 Rent Expense 5,000.00
CR 1010 Accounts Payable 5,000.00
--------- ---------
Total 5,000.00 5,000.00
Balance Sheet Impact:
Liabilities:
Accounts Payable +5,000.00
Income Statement Impact:
Expenses:
Rent Expense +5,000.00Phase 3: Reverse Journal Entry
Handler: ReverseJournalEntryCommandHandler (single) or ReverseJournalEntriesCommandHandler (batch)
Action: Creates a new journal entry with all debits and credits swapped. Used to correct or cancel a posted entry.
Prerequisites Check
| Check | Rule |
|---|---|
| Must be Posted | Status == Posted |
| Must be Manual type | Type == Manual |
| Not already reversed | IsReversed == false |
New Reversed Entry Created
The system calls JournalEntry.CreateReversedEntry() which generates a mirror image:
Table 1: JournalEntry (INSERT — Reversed Entry)
| Column Name | Value | Notes |
|---|---|---|
| Id | [New GUID] | New entry |
| Code | "JE-2026-0002" | New sequence code |
| RefrenceNumber | "REV-JE-2026-0001" | Prefixed with "REV-" |
| JournalDate | [Current Date] | Date of reversal, not original |
| Type | "Reversed" | JournalEntryType.Reversed |
| Description | "Reversal of JE-2026-0001: Monthly rent expense recognition" | System-generated |
| Status | "Posted" | Immediately posted |
| TotalDebitAmount | 5000.00 | Swapped from original credits |
| TotalCreditAmount | 5000.00 | Swapped from original debits |
| ReversedFromJournalId | [Original Journal GUID] | Points back to original |
Table 2: JournalEntryLine (INSERT × 2 — Swapped)
| Original Line | Direction | Reversed Line | Direction |
|---|---|---|---|
| Account 5020 | DR 5000 | Account 5020 | CR 5000 |
| Account 1010 | CR 5000 | Account 1010 | DR 5000 |
Reversed Journal JE-2026-0002
DR 1010 Accounts Payable 5,000.00
CR 5020 Rent Expense 5,000.00Table 3: JournalEntry (UPDATE — Original Entry)
| Column Name | Before | After | Notes |
|---|---|---|---|
| IsReversed | false | true | Marked as reversed |
| ReversedJournalId | NULL | [New Reversed GUID] | Points to reversal |
Table 4: ReversedJournalEntry (INSERT)
Operation: INSERT — tracking record for the reversal relationship
| Column Name | Value | Notes |
|---|---|---|
| JournalEntryId | [Original GUID] | Original entry |
| ReversedJournalEntryId | [Reversed GUID] | New reversal entry |
Phase 4: Edit Journal Entry
Handler: EditJournalEntryCommandHandler
Action: Updates an existing Draft journal entry. Resets status to Draft if it was changed elsewhere.
Prerequisites Check
| Check | Rule |
|---|---|
| Must be Manual type | Type == Manual |
| Must not be Posted | Status != Posted |
| Open financial period | New date must fall in open period |
Edit Payload Example
json
{
"id": "[Journal GUID]",
"refrenceNumber": "JV-001-R",
"description": "Monthly rent expense - corrected amount",
"journalEntryLines": [
{
"id": "[Existing Line 1 GUID]",
"accountId": 5020,
"debitAmount": 6000.00,
"creditAmount": 0.00,
"currencyRate": 1.0,
"costCenters": [{ "costCenterId": 87, "percentage": 100 }]
},
{
"id": "[Existing Line 2 GUID]",
"accountId": 1010,
"debitAmount": 0.00,
"creditAmount": 6000.00,
"currencyRate": 1.0
}
]
}Tables Affected
| Table | Operation | Notes |
|---|---|---|
| JournalEntry | UPDATE | RefrenceNumber, Description, TotalDebitAmount, TotalCreditAmount |
| JournalEntryLine (existing) | UPDATE | Amounts, account, description |
| JournalEntryLine (new lines) | INSERT | If new lines added |
| JournalEntryLine (removed lines) | Soft DELETE | Marked IsDeleted = true |
| JournalLineCostCenter | UPDATE / INSERT / DELETE | Sync cost center allocations |
Internal Journals (System-Generated)
Handler: AddInternalJournalCommandHandler
Action: Creates automatically-posted journal entries on behalf of other modules (Sales, Purchase, Inventory, etc.). These entries are not editable by users.
Key Differences from Manual Journals
| Property | Manual | Internal |
|---|---|---|
| Type | Manual | Sales, Purchase, Inventory, PaymentIn, PaymentOut, etc. |
| Status on creation | Draft | Posted (immediately) |
| SourceDocument | NULL | Set (e.g., SalesInvoice, StockOut) |
| SourceDocumentId | NULL | GUID of source transaction |
| Editable by user | ✅ Yes | ❌ No |
| Appears in Change-Status endpoint | ❌ Filtered out | ❌ Filtered out |
Internal Journal Payload (from Sales Invoice posting)
json
{
"refrenceNumber": "SI-2026-0001",
"journalDate": "2026-01-28T16:14:36",
"type": "Sales",
"sourceDocument": "SalesInvoice",
"sourceDocumentId": "[Invoice GUID]",
"sourceDocumentCode": "SI-2026-0001",
"description": "",
"journalEntryLines": [
{ "accountId": 1010, "debitAmount": 1150.00, "creditAmount": 0, "currencyRate": 1.0 },
{ "accountId": 2030, "debitAmount": 0, "creditAmount": 150.00, "currencyRate": 1.0 },
{ "accountId": 4010, "debitAmount": 0, "creditAmount": 1000.00, "currencyRate": 1.0,
"costCenters": [{ "costCenterId": 87, "percentage": 100 }] }
]
}Returns: JournalCreatedDto { JournalId, JournalCode }
The calling module (e.g., PostSalesInvoiceCommandHandler) stores JournalId and JournalCode back on the source document (e.g., SalesInvoiceHeader.InvoiceJournalId).
Multi-Currency Journal Entry
When a line is recorded in a foreign currency, both local and foreign amounts are stored independently.
Example — USD Invoice at 3.75 SAR/USD
json
{
"journalEntryLines": [
{
"accountId": 1010,
"debitAmount": 1000.00,
"creditAmount": 0.00,
"currencyId": 1,
"currencyRate": 3.75
},
{
"accountId": 4010,
"debitAmount": 0.00,
"creditAmount": 1000.00,
"currencyId": 1,
"currencyRate": 3.75
}
]
}Stored Values Per Line
| Column | Line 1 (AR Debit) | Line 2 (Revenue Credit) |
|---|---|---|
| DebitAmount | 1000.00 USD | 0.00 |
| CreditAmount | 0.00 | 1000.00 USD |
| CurrencyRate | 3.75 | 3.75 |
| DebitAmountLocal | 3750.00 SAR | 0.00 |
| CreditAmountLocal | 0.00 | 3750.00 SAR |
Header Totals
| Column | Value |
|---|---|
| TotalDebitAmount | 1000.00 USD (foreign) |
| TotalCreditAmount | 1000.00 USD (foreign) |
| TotalDebitAmountLocal | 3750.00 SAR (base) |
| TotalCreditAmountLocal | 3750.00 SAR (base) |
Balance check is always performed on local (base currency) amounts.
Cost Center Allocation
Certain GL accounts are configured to require cost center tracking. When a line posts to such an account, one or more cost center allocations must be provided, summing to exactly 100%.
Example — Split Cost Center Allocation
json
{
"accountId": 5020,
"debitAmount": 10000.00,
"costCenters": [
{ "costCenterId": 87, "percentage": 60 },
{ "costCenterId": 92, "percentage": 40 }
]
}JournalLineCostCenter Records
| CostCenterId | Percentage | Amount (SAR) | Notes |
|---|---|---|---|
| 87 | 60% | 6000.00 | 10000 × 60% |
| 92 | 40% | 4000.00 | 10000 × 40% |
Validation: Sum of percentages must equal 100 when the account's CostCenterConfig is set to Mandatory.
Amount recalculation: Called via JournalEntryLine.UpdateCostCentersAmount() which recomputes each allocation's Amount from the line's DebitAmountLocal or CreditAmountLocal.
Batch Operations
Batch Post
Handler: ChangeStatusJournalEntriesCommandHandler
Processes multiple journal entries at once. Returns a list of validation failures — successfully-posted entries are not included.
Returns: List<JournalValidationDto>
json
[
{ "journalId": "[GUID-A]", "message": null },
{ "journalId": "[GUID-B]", "message": "Journal entry is not balanced" },
{ "journalId": "[GUID-C]", "message": "Journal entry type is not Manual" }
]Entries with a non-null message were not posted. Only entries where message == null were successfully posted.
Batch Reverse
Handler: ReverseJournalEntriesCommandHandler
Same pattern — returns List<JournalValidationDto>. Entries that fail validation are skipped; successful reversals create new reversed journals.
Action Journal (Save / Post / Reverse in One Call)
Handler: ActionJournalEntryCommandHandler
Routes to the correct operation based on the JournalEntryAction enum:
| Action | Value | Routes To |
|---|---|---|
| Save | 0 | ChangeStatus → Draft |
| Post | 1 | ChangeStatus → Posted |
| Reverse | 2 | ReverseJournalEntries |
Unpost Journal Entry
Handler: UnpostJournalEntryCommandHandler
Changes a Posted entry back to Draft. Used for corrections before final period close.
Prerequisites
| Check | Rule |
|---|---|
| Must be Posted | Status == Posted |
| Must be Manual type | Type == Manual |
Table Update
| Column | Before | After |
|---|---|---|
| Status | "Posted" | "Draft" |
| UpdatedDate | [Previous] | [Current Timestamp] |
Delete Journal Entry
Handler: DeleteJournalEntryCommandHandler
Soft-deletes the journal entry and all its lines. The sequence number is rolled back if this was the last entry.
Tables Affected
| Table | Operation | Notes |
|---|---|---|
| JournalEntry | Soft DELETE | IsDeleted = true |
| JournalEntryLine | Soft DELETE | All lines marked deleted |
| JournalEntryAttachment | No change | Attachment service holds the file |
Constraint: Cannot delete a Posted entry. Must unpost first.
Delete Single Line
Handler: DeleteJournalEntryLineCommandHandler
Removes one line from a Draft journal entry and recalculates header totals.
Validation: At least one line must remain after deletion. The journal must remain creatable (cannot delete all lines).
Tables Affected
| Table | Operation | Notes |
|---|---|---|
| JournalEntryLine | Soft DELETE | IsDeleted = true |
| JournalEntry | UPDATE | TotalDebitAmount/TotalCreditAmount recalculated |
Journal Entry Types Reference
| Type | Value | Created By | Editable | Notes |
|---|---|---|---|---|
| Manual | 0 | User directly | ✅ Yes | Standard GL entry |
| Reversed | 1 | ReverseJournalEntry | ❌ No | Reversal of a Manual entry |
| Sales | 2 | PostSalesInvoice | ❌ No | Revenue + AR + VAT |
| Purchase | 3 | PostPurchaseInvoice | ❌ No | Expense + AP + VAT |
| Inventory | 4 | AddPostedStockOut / StockIn | ❌ No | COGS / inventory |
| PaymentIn | 5 | PostPaymentIn | ❌ No | Customer payment |
| PaymentOut | 6 | PostPaymentOut | ❌ No | Vendor payment |
| Closing | 7 | Period Close | ❌ No | Year-end close |
| FundTransfer | 8 | PostFundTransfer | ❌ No | Bank transfer |
| FixedAssets | 9 | PostFixedAsset | ❌ No | Depreciation / disposal |
| SalesReturn | 10 | PostSalesReturn | ❌ No | Credit memo |
| PurchaseReturn | 11 | PostPurchaseReturn | ❌ No | Debit memo |
Source Document Reference
When a journal entry is created internally, the SourceDocument column identifies the originating transaction type:
| SourceDocument | Created During | Links To |
|---|---|---|
| SalesInvoice | Post Sales Invoice | SalesInvoiceHeader |
| SalesReturn | Post Sales Return | SalesReturnHeader |
| PurchaseInvoice | Post Purchase Invoice | PurchaseInvoiceHeader |
| PurchaseReturn | Post Purchase Return | PurchaseReturnHeader |
| StockIn | Post Stock In | StockInHeader |
| StockOut | Post Stock Out | StockOutHeader |
| StockTransfer | Post Stock Transfer | StockTransferHeader |
| PaymentIn | Post Payment In | PaymentInHeader |
| PaymentOut | Post Payment Out | PaymentOutHeader |
| FundTransfer | Post Fund Transfer | FundTransferHeader |
| FixedAsset | Post Fixed Asset | FixedAssetHeader |
Complete Transaction Flow Summary (Manual Journal)
1. User submits Add Journal Entry request
↓
2. AddJournalEntryCommandHandler.Handle()
↓
3. Validate: reference length, date, description, period open
↓
4. Validate each line: debit XOR credit, currency rate, cost center sum
↓
5. GetCurrentPeriodQuery(JournalDate) → resolves FinancialYearPeriodId (integer DB ID, e.g. 25)
Note: command.PeriodId (string) is sent from the frontend but not used by the handler
↓
6. Generate sequence code → "JE-2026-0001"
↓
7. Confirm attachment usage in Attachment microservice
↓
8. Calculate line local amounts:
- Line 1: DebitAmountLocal = 5000 × 1.0 = 5000
- Line 2: CreditAmountLocal = 5000 × 1.0 = 5000
↓
9. Calculate header totals:
- TotalDebitAmountLocal = 5000
- TotalCreditAmountLocal = 5000
↓
10. INSERT JournalEntry (Status = Draft)
INSERT JournalEntryLine × 2
INSERT JournalLineCostCenter × 1 (Line 1)
INSERT JournalEntryAttachment × 1
↓
11. SaveChanges → Database transaction committed
↓
12. Return JournalId → Frontend receives draft entry
--- DRAFT PHASE COMPLETE ---
13. User clicks "Post" button
↓
14. ChangeStatusJournalEntriesCommandHandler.Handle()
↓
15. Validate: Manual type, Draft status, Balanced
↓
16. UPDATE JournalEntry SET Status = 'Posted'
↓
17. SaveChanges → Committed
↓
18. Return success → Entry is now part of General Ledger
--- POST PHASE COMPLETE ---
19. (Optional) User clicks "Reverse"
↓
20. ReverseJournalEntryCommandHandler.Handle()
↓
21. Validate: Posted, Manual, not already reversed
↓
22. CreateReversedEntry() → swaps DR/CR on all lines
↓
23. INSERT new JournalEntry (Type = Reversed, Status = Posted)
INSERT JournalEntryLine × 2 (swapped)
INSERT ReversedJournalEntry (relationship record)
UPDATE original JournalEntry SET IsReversed = true, ReversedJournalId = [new GUID]
↓
24. SaveChanges → Committed
↓
25. Return reversed JournalId
--- REVERSAL PHASE COMPLETE ---Database Transactions
Add Phase
sql
BEGIN TRANSACTION
INSERT INTO JournalEntry VALUES (...) -- Status = Draft
INSERT INTO JournalEntryLine VALUES (...) -- Line 1
INSERT INTO JournalEntryLine VALUES (...) -- Line 2
INSERT INTO JournalLineCostCenter VALUES (...) -- Cost center
INSERT INTO JournalEntryAttachment VALUES (...) -- Attachment
COMMIT TRANSACTIONDuration: ~30–60ms
Post Phase
sql
BEGIN TRANSACTION
UPDATE JournalEntry
SET Status = 'Posted',
UpdatedDate = GETUTCDATE(),
UpdatedBy = @UserId
WHERE Id = @JournalId
COMMIT TRANSACTIONDuration: ~10–20ms
Reverse Phase
sql
BEGIN TRANSACTION
INSERT INTO JournalEntry VALUES (...) -- Reversed entry, Posted
INSERT INTO JournalEntryLine VALUES (...) -- Swapped lines
INSERT INTO ReversedJournalEntry VALUES (...) -- Relationship
UPDATE JournalEntry
SET IsReversed = true, ReversedJournalId = @NewGuid
WHERE Id = @OriginalId
COMMIT TRANSACTIONDuration: ~50–100ms
Error Scenarios
Add / Edit Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Closed financial period | "Journal date must be in an open financial period" | Change date or open period |
| Both debit and credit non-zero | "A line cannot have both debit and credit amounts" | Set only one per line |
| Both debit and credit zero | "A line must have either a debit or credit amount" | Provide at least one |
| Currency rate missing | "Currency rate is required" | Provide exchange rate |
| Cost center sum ≠ 100% | "Cost center allocations must total 100%" | Adjust percentages |
| Reference number too long | "Reference number must be 15 characters or less" | Shorten reference |
Post Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Not Manual type | "Journal entry type is not Manual" | Cannot manually post system journals |
| Not Draft | "Journal entry is not in Draft status" | Already posted or reversed |
| Not balanced | "Journal entry is not balanced" | Fix debit/credit totals to match |
Reverse Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Not Posted | "Journal entry must be Posted to reverse" | Post first |
| Not Manual | "Only Manual journal entries can be reversed" | Cannot reverse system journals |
| Already reversed | "Journal entry has already been reversed" | Each entry can only be reversed once |
Workflow Integration
Journal entries support optional approval workflows via the IWorkflowEnabledCommand interface.
| Property | Description |
|---|---|
| EnableWorkflow | Whether to trigger the approval workflow |
| TransactionId | Workflow transaction reference |
| ModuleId | Modules.Accounting |
| ServiceId | Services.JournalEntries |
| ServiceActionId | ActionTypes.Save |
When workflow is enabled:
- Entry is saved with
WorkflowStatus = Pending - Approvers are notified via the Workflow microservice
- Entry can only be posted after approval
Query / List Behavior
GetAllJournalEntry Filters
| Filter | Type | Behavior |
|---|---|---|
| SearchTerm | string | Matches Code or RefrenceNumber |
| FromDate / ToDate | DateTime? | Filters on JournalDate |
| Type | List<JournalEntryType> | Multi-select filter |
| Status | List<JournalEntryTransactionStatus> | Draft / Posted / Unbalanced |
| SourceDocument | List<SourceDocument> | Filter by originating module |
| CreatedBy | List<string> | Filter by user name |
Note: Entries with Type = PaymentIn or Type = PaymentOut that are in Draft status are excluded from the list. Only their posted state or their reversal entries are shown. This prevents payment journals from appearing before the payment is finalized.
JournalEntryTransactionStatus (special enum for UI)
| Value | Name | Meaning |
|---|---|---|
| 1 | Draft | Status == Draft |
| 2 | Posted | Status == Posted |
| 3 | Unbalanced | TotalDebitAmountLocal ≠ TotalCreditAmountLocal |
Key Interfaces Used
| Interface | Usage |
|---|---|
IAppsUnitOfWork | Repository access for all ERP entities |
ISecurityService | Current user, tenant context, permission checks |
IClockService | Testable timestamp provider (GetUtcNow()) |
IGeneralSettingsPublicApi | Sequence generation, line description settings |
IAccountingPublicApi | Called by other modules to create internal journals |
Document Version: 1.0
Namespace: AppsPortal.Application.Accounting.GeneralLedger.JournalEntries
Example Date: 2026-01-28
Currency: Saudi Riyal (SAR)