Skip to content

Commit

Permalink
When voiding an entry, add option to use date of original entry for t…
Browse files Browse the repository at this point in the history
…he voiding entry, instead of the current date. #117 (#118)

* adding option to use_original_date when voiding a journal entry & test

* clarified spec

* prettified

* explain use_original_date and known issue

---------

Co-authored-by: TudorUptrade <[email protected]>
  • Loading branch information
tudorelu and tudorUptrade authored Nov 28, 2024
1 parent 251129b commit be2f46e
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 4 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ await myBook.void("5eadfd84d7d587fb794eaacb", "I made a mistake");

If you do not specify a void reason, the system will set the memo of the new journal to the original journal's memo prepended with "[VOID]".

By default, voided journals will have the `datetime` set to the current date and time (at the time of voiding). Optionally, you can set `use_original_date` to `true` to use the original journal's `datetime` instead (`book.void(journal_id, void_reason, {}, true)`).

**Known Issue:** Note that, when using the original date, the cached balances will be out of sync until they are recalculated (within 48 hours at most). Check this [discussion](https://github.com/flash-oss/medici/issues/117) for more details.

## ACID checks of an account balance

Sometimes you need to guarantee that an account balance never goes negative. You can employ MongoDB ACID transactions for that. As of 2022 the recommended way is to use special Medici writelock mechanism. See comments in the code example below.
Expand Down
67 changes: 67 additions & 0 deletions spec/book.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,73 @@ describe("book", function () {

await book.void(journal._id.toString());
});

it("should have balance unchanged immediately after event date, when voiding event", async () => {
const justBeforeDate = new Date("2000-02-20T00:01:00.000Z");
const date = new Date("2000-02-20T00:02:00.000Z");
const justAfterDate = new Date("2000-02-20T00:03:00.000Z");
const oneDayAfter = new Date("2000-02-22T00:04:05.000Z");
const journal = await book
.entry("Test Entry", date)
.debit("Assets:ReceivableVoid", 700)
.credit("Income:RentVoid", 700)
.commit();

const balanceAfterEvent = await book.balance({
account: "Assets:ReceivableVoid",
start_date: justBeforeDate,
end_date: justAfterDate,
});
expect(balanceAfterEvent.balance).to.be.equal(-700);

await book.void(journal._id.toString());

// need to delete the balance snapshots to test the balance after as the query is similar
await balanceModel.deleteMany({ book: book.name });

const balanceAfterVoidingBeforeVoidingDate = await book.balance({
account: "Assets:ReceivableVoid",
start_date: justBeforeDate,
end_date: oneDayAfter,
});
expect(balanceAfterVoidingBeforeVoidingDate.balance).to.be.equal(-700);

await balanceModel.deleteMany({ book: book.name });

const balanceAfterVoidingAfterVoidingDate = await book.balance({ account: "Assets:ReceivableVoid" });
expect(balanceAfterVoidingAfterVoidingDate.balance).to.be.equal(0);
});

it("should have balance changed immediately after event date, when void journal keeps date of original journal", async () => {
const justBeforeDate = new Date("2000-02-20T00:01:00.000Z");
const date = new Date("2000-02-20T00:02:00.000Z");
const justAfterDate = new Date("2000-02-20T00:03:00.000Z");
const oneDayAfter = new Date("2000-02-22T00:04:05.000Z");
const journal = await book
.entry("Test Entry", date)
.debit("Assets:ReceivableVoid2", 700)
.credit("Income:RentVoid2", 700)
.commit();

const balanceBeforeVoiding = await book.balance({
account: "Assets:ReceivableVoid2",
start_date: justBeforeDate,
end_date: justAfterDate,
});
expect(balanceBeforeVoiding.balance).to.be.equal(-700);

await book.void(journal._id.toString(), undefined, undefined, true);

// need to delete the balance snapshots to test the balance after void
await balanceModel.deleteMany({ book: book.name });

const balanceAfterVoidingAfterEventDate = await book.balance({
account: "Assets:ReceivableVoid2",
start_date: justAfterDate,
end_date: oneDayAfter,
});
expect(balanceAfterVoidingAfterEventDate.balance).to.be.equal(0);
});
});

describe("listAccounts", () => {
Expand Down
14 changes: 10 additions & 4 deletions src/Book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ export class Book<U extends ITransaction = ITransaction, J extends IJournal = IJ
const partialBalanceOptions = { ...options };
// If using a balance snapshot then make sure to use the appropriate (default "_id_") index for the additional balance calc.
if (parsedQuery._id && balanceSnapshot) {
const lastTransactionDate = balanceSnapshot.transaction.getTimestamp();
const lastTransactionDate = balanceSnapshot.transaction.getTimestamp();
if (lastTransactionDate.getTime() + this.expireBalanceSnapshotSec * 1000 > Date.now()) {
// last transaction for this balance was just recently, then let's use the "_id" index as it will likely be faster than any other.
partialBalanceOptions.hint = { _id: 1 };
partialBalanceOptions.hint = { _id: 1 };
}
}
const result = (await transactionModel.collection.aggregate([match, GROUP], partialBalanceOptions).toArray())[0];
Expand Down Expand Up @@ -229,7 +229,12 @@ export class Book<U extends ITransaction = ITransaction, J extends IJournal = IJ
};
}

async void(journal_id: string | Types.ObjectId, reason?: undefined | string, options = {} as IOptions) {
async void(
journal_id: string | Types.ObjectId,
reason?: undefined | string,
options = {} as IOptions,
use_original_date = false
) {
journal_id = typeof journal_id === "string" ? new Types.ObjectId(journal_id) : journal_id;

const journal = await journalModel.collection.findOne(
Expand All @@ -245,6 +250,7 @@ export class Book<U extends ITransaction = ITransaction, J extends IJournal = IJ
memo: true,
void_reason: true,
voided: true,
datetime: true,
},
}
);
Expand All @@ -266,7 +272,7 @@ export class Book<U extends ITransaction = ITransaction, J extends IJournal = IJ
throw new MediciError(`Transactions for journal ${journal._id} not found on book ${journal.book}`);
}

const entry = this.entry(reason, null, journal_id);
const entry = this.entry(reason, use_original_date ? journal.datetime : null, journal_id);

addReversedTransactions(entry, transactions as ITransaction[]);

Expand Down

0 comments on commit be2f46e

Please sign in to comment.