Giter Site home page Giter Site logo

medici's Introduction

medici

Build Status Known Vulnerabilities Security Responsible Disclosure NPM version NPM downloads

Double-entry accounting system for nodejs + mongoose

npm i medici

Basics

To use Medici you will need a working knowledge of JavaScript, Node.js, and Mongoose.

Medici divides itself into "books", each of which store journal entries and their child transactions. The cardinal rule of double-entry accounting is that "for every debit entry, there must be a corresponding credit entry" which means "everything must balance out to zero", and that rule is applied to every journal entry written to the book. If the transactions for a journal entry do not balance out to zero, the system will throw a new error with the message INVALID JOURNAL.

Books simply represent the physical book in which you would record your transactions - on a technical level, the "book" attribute simply is added as a key-value pair to both the Medici_Transactions and Medici_Journals collection to allow you to have multiple books if you want to.

Each transaction in Medici is for one account. Additionally, sub accounts can be created, and are separated by a colon. Transactions to the Assets:Cash account will appear in a query for transactions in the Assets account, but will not appear in a query for transactions in the Assets:Property account. This allows you to query, for example, all expenses, or just "office overhead" expenses (Expenses:Office Overhead).

In theory, the account names are entirely arbitrary, but you will likely want to use traditional accounting sections and subsections like assets, expenses, income, accounts receivable, accounts payable, etc. But, in the end, how you structure the accounts is entirely up to you.

Limitations:

  • You can safely add values up to 9007199254740991 (Number.MAX_SAFE_INTEGER) and by default down to 0.00000001 (precision: 8).
  • Anything more than 9007199254740991 or less than 0.00000001 (precision: 8) is not guaranteed to be handled properly.

You can set the floating point precision as follows:

const myBook = new Book("MyBook", { precision: 7 });

Writing journal entries

Writing a journal entry is very simple. First you need a book object:

const { Book } = require("medici");

// The first argument is the book name, which is used to determine which book the transactions and journals are queried from.
const myBook = new Book("MyBook");

Now write an entry:

// You can specify a Date object as the second argument in the book.entry() method if you want the transaction to be for a different date than today
const journal = await myBook
  .entry("Received payment")
  .debit("Assets:Cash", 1000)
  .credit("Income", 1000, { client: "Joe Blow" })
  .commit();

You can continue to chain debits and credits to the journal object until you are finished. The entry.debit() and entry.credit() methods both have the same arguments: (account, amount, meta).

You can use the "meta" field which you can use to store any additional information about the transaction that your application needs. In the example above, the client attribute is added to the transaction in the Income account, so you can later use it in a balance or transaction query to limit transactions to those for Joe Blow.

Querying Account Balance

To query account balance, just use the book.balance() method:

const { balance } = await myBook.balance({
  account: "Assets:Accounts Receivable",
  client: "Joe Blow",
});
console.log("Joe Blow owes me", balance);

Note that the meta query parameters are on the same level as the default query parameters (account, _journal, start_date, end_date). Medici parses the query and automatically turns any values that do not match top-level schema properties into meta parameters.

Retrieving Transactions

To retrieve transactions, use the book.ledger() method (here I'm using moment.js for dates):

const startDate = moment().subtract("months", 1).toDate(); // One month ago
const endDate = new Date(); // today

const { results, total } = await myBook.ledger({
  account: "Income",
  start_date: startDate,
  end_date: endDate,
});

Voiding Journal Entries

Sometimes you will make an entry that turns out to be inaccurate or that otherwise needs to be voided. Keeping with traditional double-entry accounting, instead of simply deleting that journal entry, Medici instead will mark the entry as "voided", and then add an equal, opposite journal entry to offset the transactions in the original. This gives you a clear picture of all actions taken with your book.

To void a journal entry, you can either call the void(void_reason) method on a Medici_Journal document, or use the book.void(journal_id, void_reason) method if you know the journal document's ID.

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]".

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.

import { Book, mongoTransaction } from "medici";

const mainLedger = new Book("mainLedger");

async function withdraw(walletId: string, amount: number) {
  return mongoTransaction(async (session) => {
    await mainLedger
      .entry("Withdraw by User")
      .credit("Assets", amount)
      .debit(`Accounts:${walletId}`, amount)
      .commit({ session });

    // .balance() can be a resource-expensive operation. So we do it after we
    // created the journal.
    const balanceAfter = await mainLedger.balance(
      {
        account: `Accounts:${walletId}`,
      },
      { session }
    );

    // Avoid spending more than the wallet has.
    // Reject the ACID transaction by throwing this exception.
    if (balanceAfter.balance < 0) {
      throw new Error("Not enough balance in wallet.");
    }

    // ISBN: 978-1-4842-6879-7. MongoDB Performance Tuning (2021), p. 217
    // Reduce the Chance of Transient Transaction Errors by moving the
    // contentious statement to the end of the transaction.

    // We writelock only the account of the User/Wallet. If we writelock a very
    // often used account, like the fictitious Assets account in this example,
    // we would slow down the database extremely as the writelocks would make
    // it impossible to concurrently write in the database.
    // We only check the balance of the User/Wallet, so only this Account has to
    // be writelocked.
    await mainLedger.writelockAccounts([`Accounts:${walletId}`], { session });
  });
}

Document Schema

Journals are schemed in Mongoose as follows:

JournalSchema = {
  datetime: Date,
  memo: {
    type: String,
    default: "",
  },
  _transactions: [
    {
      type: Schema.Types.ObjectId,
      ref: "Medici_Transaction",
    },
  ],
  book: String,
  voided: {
    type: Boolean,
    default: false,
  },
  void_reason: String,
};

Transactions are schemed as follows:

TransactionSchema = {
  credit: Number,
  debit: Number,
  meta: Schema.Types.Mixed,
  datetime: Date,
  account_path: [String],
  accounts: String,
  book: String,
  memo: String,
  _journal: {
    type: Schema.Types.ObjectId,
    ref: "Medici_Journal",
  },
  timestamp: Date,
  voided: {
    type: Boolean,
    default: false,
  },
  void_reason: String,
  // The journal that this is voiding, if any
  _original_journal: Schema.Types.ObjectId,
};

Note that the book, datetime, memo, voided, and void_reason attributes are duplicates of their counterparts on the Journal document. These attributes will pretty much be needed on every transaction search, so they are added to the Transaction document to avoid having to populate the associated Journal every time.

Customizing the Transaction document schema

If you need to add additional fields to the schema that the meta won't satisfy, you can define your own schema for Medici_Transaction and utilise the setJournalSchema and setTransactionSchema to use those schemas. When you specify meta values when querying or writing transactions, the system will check the Transaction schema to see if those values correspond to actual top-level fields, and if so will set those instead of the corresponding meta field.

For example, if you want transactions to have a related "person" document, you can define the transaction schema like so and use setTransactionSchema to register it:

MyTransactionSchema = {
  _person: {
    type: Schema.Types.ObjectId,
    ref: "Person",
  },
  credit: Number,
  debit: Number,
  meta: Schema.Types.Mixed,
  datetime: Date,
  account_path: [String],
  accounts: String,
  book: String,
  memo: String,
  _journal: {
    type: Schema.Types.ObjectId,
    ref: "Medici_Journal",
  },
  timestamp: Date,
  voided: {
    type: Boolean,
    default: false,
  },
  void_reason: String,
};

// add an index to the Schema
MyTransactionSchema.index({ void: 1, void_reason: 1 });

// assign the Schema to the Model
setTransactionSchema(MyTransactionSchema, undefined, { defaultIndexes: true });

// Enforce the index 'void_1_void_reason_1'
await syncIndexes({ background: false });

Performance

Fast balance

In medici v5 we introduced the so-called "fast balance" feature. Here is the discussion. TL;DR: it caches .balance() call result once a day (customisable) to medici_balances collection.

If a database has millions of records then calculating the balance on half of them would take like 5 seconds. When this result is cached it takes few milliseconds to calculate the balance after that.

How it works under the hood

There are two hard problems in programming: cache invalidation and naming things. (C) Phil Karlton

Be default, when you call book.blanace(...) for the first time medici will cache its result to medici_balances (aka balance snapshot). By default, every doc there will be auto-removed as they have TTL of 48 hours. Meaning this cache will definitely expire in 2 days. Although, medici will try doing a second balance snapshot every 24 hours (default value). Thus, at any point of time there will be present from zero to two snapshots per balance query.

When you would call the book.balance(...) with the same exact arguments the medici will:

  • retrieve the most recent snapshot if present,
  • sum up only transactions inserted after the snapshot, and
  • add the snapshot's balance to the sum.

In a rare case you wanted to remove some ledger entries from medici_transactions you would also need to remove all the medici_balances docs. Otherwise, the .balance() would be returning inaccurate data for up to 24 hours.

IMPORTANT!

To make this feature consistent we had to switch from client-generated IDs to MongoDB server generated IDs. See forceServerObjectId.

How to disable balance caching feature

When creating a book you need to pass the balanceSnapshotSec: 0 option.

const myBook = new Book("MyBook", { balanceSnapshotSec: 0 })

Indexes

Medici adds a few default indexes on the medici_transactions collection:

    "_journal": 1
    "book": 1,
    "accounts": 1,
    "datetime": -1,
    "book": 1,
    "account_path.0": 1,
    "account_path.1": 1,
    "account_path.2": 1,
    "datetime": -1,

However, if you are doing lots of queries using the meta data you probably would want to add the following index(es):

    "book": 1,
    "accounts": 1,
    "meta.myClientId": 1,
    "datetime": -1,

and/or

    "book": 1,
    "meta.myClientId": 1,
    "account_path.0": 1,
    "account_path.1": 1,
    "account_path.2": 1,
    "datetime": -1,

Here is how to add an index manually via MongoDB CLI or other tool:

db.getSiblingDB("my_db_name").getCollection("medici_transactions").createIndex({
    "book": 1,
    "accounts": 1,
    "meta.myClientId": 1,
    "datetime": -1,
}, { background: true })

For more information, see Performance Best Practices: Indexing

Changelog

7.0

Unluckily, all the default indexes were suboptimal. The book property always had the lowest cardinality. However, we always query by the book first and then by some other properties. Thus all the default indexes were near useless.

This release fixes the unfortunate mistake.

  • The book property cardinality was moved to the beginning of all default indexes.
  • Most of the default indexes were useless in majority of use cases. Thus, were removed. Only 4 default indexes left for medici v7.0:
    • _id
    • _journal
    • book,accounts,datetime
    • book,account_path.0,account_path.1,account_path.2,datetime
  • The datetime is the only one to be used in the default indexes. Additional timestamp doesn't make any sense.
  • Removed the book.listAccounts() caching which was added in the previous release (v6.3). The default indexes cover this use case now. Moreover, the index works faster than the cache.

6.3

  • The book.listAccounts() method is now cached same way the book.balance() is cached.
  • Add mongoose v8 support.

6.2

  • Add mongoose v7 support.
  • Add Node 20 support.

6.1

  • Add MongoDB v6 support.

6.0

  • Drop node 12 and 14 support. Only 16 and 18 are supported now.
  • By default use the secondary nodes (if present) of your MongoDB cluster to calculate balances.

v5.2

  • The balances cache primary key is now a SHA1 hash of the previous value. Before: "MyBook;Account;clientId.$in.0:12345,clientId.$in.1:67890,currency:USD". After: "\u001b\u0004Nรžj\u0013rร…\u001bยผ,F_#\u001cร”k Nv". Allows each key to be exactly 40 bytes (20 chars) regardless the actual balance query text length.
    • But the old raw unhashed key is now stored in rawKey of medici_balances for DX and troubleshooting purposes.
  • Fixed important bugs #58 and #70 related to retrieving balance for a custom schema properties. Thanks @dolcalmi

v5.1

The balance snapshots were never recalculated from the beginning of the ledger. They were always based on the most recent snapshot. It gave us speed. Although, if one of the snapshots gets corrupt or an early ledger entry gets manually edited/deleted then we would always get wrong number from the .balance() method. Thus, we have to calculate snapshots from the beginning of the ledger at least once in a while.

BUT! If you have millions of documents in medici_transactions collection a full balance recalculation might take up to 10 seconds. So, we can't afford aggregation of the entire database during the .blance() invocation. Solution: let's aggregate it in the background. Thus, v5.1 was born.

New feature:

  • In addition to the existing balanceSnapshotSec option, we added expireBalanceSnapshotSec.
    • The balanceSnapshotSec tells medici how often you want those snapshots to be made in the background (right after the .balance() call). Default value - 24 hours.
    • The expireBalanceSnapshotSec tells medici when to evict those snapshots from the database (TTL). It is recommended to set expireBalanceSnapshotSec higher than balanceSnapshotSec. Default value - twice the balanceSnapshotSec.

v5.0

High level overview.

  • The project was rewritten with TypeScript. Types are provided within the package now.
  • Added support for MongoDB sessions (aka ACID transactions). See IOptions type.
  • Did number of consistency, stability, server disk space, and speed improvements. Balance querying on massive databases with millions of documents are going to be much-much faster now. Like 10 to 1000 times faster.
  • Mongoose v5 and v6 are both supported now.

Major breaking changes:

  • The "approved" feature was removed.
  • Mongoose middlewares on medici models are not supported anymore.
  • The .balance() method does not support pagination anymore.
  • Rename constructor book -> Book.
  • Plus some other potentially breaking changes. See below.

Step by step migration from v4 to v5.

  • Adapt your code to all the breaking changes.
  • On the app start medici (actually mongoose) will create all the new indexes. If you have any custom indexes containing the approved property, you'd need to create similar indexes but without the property.
  • You'd need to manually remove all the indexes which contain approved property in it.
  • Done.

All changes of the release.

  • Added a mongoTransaction-method, which is a convenience shortcut for mongoose.connection.transaction.
  • Added async helper method initModels, which initializes the underlying transactionModel and journalModel. Use this after you connected to the MongoDB-Server if you want to use transactions. Or else you could get Unable to read from a snapshot due to pending collection catalog changes; please retry the operation. error when acquiring a session because the actual database-collection is still being created by the underlying mongoose-instance.
  • Added syncIndexes. Warning! This function will erase any custom (non-builtin) indexes you might have added.
  • Added setJournalSchema and setTransactionSchema to use custom Schemas. It will ensure, that all relevant middlewares and methods are also added when using custom Schemas. Use syncIndexes-method from medici after setTransactionSchema to enforce the defined indexes on the models.
  • Added maxAccountPath. You can set the maximum amount of account paths via the second parameter of Book. This can improve the performance of .balance() and .ledger() calls as it will then use the accounts attribute of the transactions as a filter.
  • MongoDB v4 and above is supported. You can still try using MongoDB v3, but it's not recommended.
  • Added a new timestamp+datetime index on the transactionModel to improve the performance of paginated ledger queries.
  • Added a lockModel to make it possible to call .balance() and get a reliable result while using a mongo-session. Call .writelockAccounts() with first parameter being an Array of Accounts, which you want to lock. E.g. book.writelockAccounts(["Assets:User:User1"], { session }). For best performance call writelockAccounts as the last operation in the transaction. Also .commit() accepts the option writelockAccounts, where you can provide an array of accounts or a RegExp. It is recommended to use the book.writelockAccounts().
  • POTENTIALLY BREAKING: Node.js 12 is the lowest supported version. Although, 10 should still work fine.
  • POTENTIALLY BREAKING: MongoDB v4.0 is the lowest supported version. The v3.6 support was dropped.
  • POTENTIALLY BREAKING: .ledger() returns lean Transaction-Objects (POJO) for better performance. To retrieve hydrated mongoose models set lean to false in the third parameter of .ledger(). It is recommended to not hydrate the transactions, as it implies that the transactions could be manipulated and the data integrity of Medici could be risked.
  • POTENTIALLY BREAKING: Rounding precision was changed from 7 to 8 floating point digits.
    • The new default precision is 8 digits. The medici v4 had it 7 by default. Be careful if you are using values which have more than 8 digits after the comma.
    • You can now specify the precision in the Book constructor as an optional second parameter precision. Simulating medici v4 behaviour: new Book("MyBook", { precision: 7 }).
    • Also, you can enforce an "only-Integer" mode, by setting the precision to 0. But keep in mind that Javascript has a max safe integer limit of 9007199254740991.
  • POTENTIALLY BREAKING: Added validation for name of Book, maxAccountPath and precision.
    • The name has to be not an empty string or a string containing only whitespace characters.
    • precision has to be an integer bigger or equal 0.
    • maxAccountPath has to be an integer bigger or equal 0.
  • POTENTIALLY BREAKING: Added prototype-pollution protection when creating entries. Reserved words like __proto__ can not be used as properties of a Transaction or a Journal or their meta-Field. They will get silently filtered out.
  • POTENTIALLY BREAKING: When calling book.void() the provided journal_id has to belong to the book. If the journal does not exist within the book, medici will throw a JournalNotFoundError. In medici < 5 you could theoretically void a journal of another book.
  • POTENTIALLY BREAKING: Transaction document properties meta, voided, void_reason, _original_journal won't be stored to the database when have no data. In medici v4 they were {}, false, null, null correspondingly.
  • BREAKING: If you had any Mongoose middlewares (e.g. "pre save") installed onto medici transactionModel or journalModel then they won't work anymore. Medici v5 is not using the mongoose to do DB operations. Instead, we execute commands via bare mongodb driver.
  • BREAKING: .balance() does not support pagination anymore. To get the balance of a page sum up the values of credit and debit of a paginated .ledger()-call.
  • BREAKING: You can't import book anymore. Only Book is supported. require("medici").Book.
  • BREAKING: The approving functionality (approved and setApproved()) was removed. It's complicating code, bloating the DB, not used by anyone maintainers know. Please, implement approvals outside the ledger. If you still need it to be part of the ledger then you're out of luck and would have to (re)implement it yourself. Sorry about that.

v4.0

  • Node.js 8 is required now.
  • Drop support of Mongoose v4. Only v5 is supported now. (But v4 should just work, even though not tested.)
  • No API changes.

v3.0

  • Add 4 mandatory indexes, otherwise queries get very slow when transactions collection grows.
  • No API changes.

v2.0

  • Upgrade to use mongoose v5. To use with mongoose v4 just npm i medici@1.
  • Support node.js v10.
  • No API changes.

v1.0

See this PR for more details

  • BREAKING: Dropped support of node.js v0.10, v0.12, v4, and io.js. Node.js >= v6 is supported only. This allowed to drop several production dependencies. Also, few bugs were automatically fixed.
  • BREAKING: Upgraded mongoose to v4. This allows medici to be used with wider mongodb versions.
  • Dropped production dependencies: moment, q, underscore.
  • Dropped dev dependencies: grunt, grunt-exec, grunt-contrib-coffee, grunt-sed, grunt-contrib-watch, semver.
  • No .coffee any more. Using node.js v6 compatible JavaScript only.
  • There are no API changes.
  • Fixed a bug. Transaction meta data was not voided correctly.
  • This module maintainer is now flash-oss instead of the original author jraede.

medici's People

Contributors

adam2k avatar dependabot[bot] avatar dolcalmi avatar jraede avatar koresar avatar nicolasburtey avatar olamilekan000 avatar torchedmedia avatar uzlopak avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

medici's Issues

Can we know if Debit balance is intentionally given as "negative" value ?

Hi ,

Thank you for helping to find the Balance. However, I would like to know if my AccountReceivable:GivenAccount has a debit value of 11,371.66.

But when I retrieve the balance I get a negative number in the account balance query = -11,371.66

Can I know if a Negative Balance always means "debit value" when we Querying Account Balance?

Screenshot 2024-03-08 at 12 05 44

start_date and end_date filter on balance API doesn't work correctly if an exisiting balance snapshot is present

Hi Medici team,
It seems that the balance API doesn't return the correct values if there is a cached balance entry in balance_snapshots.
If the start_date and end_date are values before the last transaction Id in the snapshot, the balance API just returns the balance in the snapshot.
link

I suggest that we should also maintain a start_time and end_time in the balance snapshot and check if the range query can be satisfied by the snapshot. Else recalculate accordingly.
Happy to raise a PR for the same if we agree on a solution.
Or please suggest a work-around which doesn't turn the caching feature completely off.

Thanks

new release

@koresar

@adam2k and I worked on medici and we modified it accordingly to fix the sec. vuln. of mongoose.

Can you please make a release?

Invalid entry gets accepted

I have the following code

  const myBook = new book("test");

  let entry = myBook.entry("This is a test entry");

  entry.debit("Assets:Cash", 99.9, {});
  entry.credit("Income", 99.8, {});

  entry.commit().then(journal => console.log(journal));

which leads to a valid journal entry instead of raising an INVALID JOURNAL exception, as expected.


{ __v: 0,
  book: 'test',
  datetime: 2018-03-02T10:36:24.244Z,
  _id: 5a992928b7323207c3713991,
  approved: true,
  voided: false,
  _transactions: [ 5a992928b7323207c3713992, 5a992928b7323207c3713993 ],
  memo: 'This is a test entry' }

Example in readme is broken

trying this example:

const startDate = moment().subtract("months", 1).toDate(); // One month ago
const endDate = new Date(); // today

const transactions = await myBook.ledger({
  account: "Income",
  start_date: startDate,
  end_date: endDate
});

I get:

CastError: Cast to date failed for value "Invalid Date" at path "datetime" for model "Medici_Transaction"
    at model.Query.exec (/Users/anentropic/Documents/Dev/Work/balance-demo/node_modules/mongoose/lib/query.js:4437:21)
    at Book.ledger (/Users/anentropic/Documents/Dev/Work/balance-demo/node_modules/medici/src/book.js:212:30)
    at main (/Users/anentropic/Documents/Dev/Work/balance-demo/pay_periods.js:32:35)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  messageFormat: undefined,
  stringValue: '"Invalid Date"',
  kind: 'date',
  value: Invalid Date,
  path: 'datetime',
  reason: AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:

    assert.ok(!isNaN(value.valueOf()))

      at castDate (/Users/anentropic/Documents/Dev/Work/balance-demo/node_modules/mongoose/lib/cast/date.js:13:12)

Same if I use Date for both with no moment.js stuff.

It turns out the fix is to pass start_date and end_date as strings instead of Date instances, i.e.:

  const transactions = await myBook.ledger({
    account: "Income",
    start_date: "2021-01-01T12:00:00",
    end_date: "2021-05-01T08:00:00"
  });
  console.log(transactions);

...works fine.

This is with medici-4.0.1 mongoose-5.12.7 and MongoDB 4.2

Value Added Tax

Hello
Great job!
Does Medici supports multiple VAT?
Thanks

Getting Error in Local Dev Server

Hello,

I keep getting the below error, when I try to run the example code:

TransactionError: Failure to save journal: Operation medici_transactions.insertMany() buffering timed out after 10000ms
at Entry.commit in medici/build/Entry.js

The code I'm using is as below:

const { Book } = require("medici");
const journal = new Book("My Book");
const amount = 10000;
await journal.entry("New Entry")
.debit("Incomes:Sales", amount, {narration: "Debit Entry"})
.credit("Expenses:Purchases", amount, {narration: "Credit Entry"})
.commit();

Could you please help me identify what am I doing wrong here?

voided invoice issue

It seems invoiced are being voided for the transaction collection but not marked as voided on the journal collection. I'll make more tests and provide a PR if that the case.

performance with index

Medici is slow when the number of records reach 30k

I think the Readme is misleading.

I'm assuming there is no performance issue over 30k record when running index.

Is that correct?

related to #12

Mongoose Specify Connection

How can I specify the connection to use ?

For example, when connection is achieved by mongoose.createConnection(...) vs mongoose.connect(...).

The project I am working on makes use of connections returned from the createConnection method, and not via the standard mongoose connection pool.

Cant find any documentation or outline in your readme on where the connection can be specified

write is not a function

I have tried running the code snippet in the README but it says that "write is not a function". The code i tried is:

const { book } = require('medici');

const myBook = new book('TEST');

myBook.entry('cool')
.debit('Assets:Cash', 1000)
.credit('Income', 1000, { client: 'Sameer' })
.write().then(result => {
console.log(result);
});

After upgrading to 5.1.0 retrieving balance using custom properties as keys returns 0

@koresar
Thank you for this module.

We have created a test program that demonstrates this. We have introduced two custom properties: _businessId and _assetId.

  • Without using a custom schema retrieving a book balance using these custom properties as keys returns the correct result.
  • When using a custom schema (line 15 in app.js is uncommented), retrieving book balance without using any of the custom properties works correctly (see _retrieveBalance() in app.js), but retrieving balance using either one or two of the custom properties as keys returns 0 (see _retrieveBalanceWithCustomProperties1() and _retrieveBalanceWithCustomProperties2() in app.js)

For the 3 test cases the correct balance should be

  • -2000 for _retrieveBalance()
  • 2000 for both _retrieveBalanceWithCustomProperties1() and _retrieveBalanceWithCustomProperties2()

To run without custom schema defined (ensure line 15 in app.js is commented out)
Tun run with custom schema defined (ensure line 15 in app.js is uncommented)

Possibly we are doing something wrong in our schema definition. Thanks in advance for your help.

medici-v5.zip

Timestamp

@koresar

The failed unit test made me thinking. Despite we lost the validation functionality this actually means that you could overwrite the timestamp and set it something in the future or in the past, which would actually violate the "rule of good bookkeeping". Also could mean that a persisted balance could be breaken, by setting a debit into very past. Didn't thought of it before.

Implicitly allowing to set the timestamp arbitrary is probably not good.

Maybe add an timestamp attribute to Entry.ts and use it when creating each transaction, thus avoiding diverging timestamps by milliseconds and then in the line before we push the transaction into the transactint array assign the Entry timestamp to the transaction timestamp.

v5 Discussion Thread

@koresar

I made some changes to the typescript branch. I removed node 10 support, as it is EOL and added node 16 as it is current LTS.

The branch itself uses mongoose 6. Maybe it is better to make it a peerDependency? But this should resolve #30

I also added an optional Options parameter to "sessionable" methods to pass a mongo session. I think there is an issue with the pre save hook regarding sessions. Should resolve #23

The API itself is not breaking. But removed node 10 support and usage of mongoose 6 is definitely breaking change and not avoidable.

I personally dont use prettier, so I dont have any configuration which I could add to it.

Example Project

Is there an example project of how this is used? I'm debating between this and using SQL.

meta data is not copied on voided Transactions

Hi,

not sure if this library is still actively maintained. I believe I have come across a bug where transactions which have meta tags are not voided correctly. The extra ledger entry which is created to reverse the original transaction should have the same meta data as the original entries. if this is not the case - then balance queries based on meta tags are incorrect when a transaction has been voided.

The commit below shows a fix and updated test case. I also upgraded the version of a few libraries.

mos-flash@729253b

I'm fairly new to JS and coffee script so feedback on changes is welcome.

RFC: Medici is slow when number of records reach 30k, add indexes

The balance queries take 100 - 400 ms average. The MongoDB Cloud Performance Adviser shows these fields are good candidates for indexing:

account_path.0: 1
account_path.1: 1
account_path.2: 1
book: 1
approved: 1

The Medici v2.1 should be released and set the indexes by default.

last version not released?

hey guys @maintenairs.

there are been a couple of merged PR that we'd like to use, but medici has not been released following those PR merged. is that something you can do?

Missing changelog for 6.0.0

I'm assuming 6.0.0 brings support for mongodb version 6, and would like to upgrade to it. It would be great to have a changelog in the readme for 6.0.0 like there is for other versions that has the breaking changes mentioned.

multi currency account

If you were to implement a multi-currency version of Medici, how would you do it?

I guess

.debit('Assets:Cash', 1000, {meta}) would need another parameters, ie: .debit('Assets:Cash', 1000, {meta}, "USD")

A transaction would still equal out with multiple currencies. ie Alice sending 10 USD for 12 EUR to Bob:

.credit('Alice', 10,  {}, 'USD')
.debit('Bob", 10, {}, USD')
.debit('Alice', 12,  {}, 'EUR')
.credit('Alice, 12, {}, 'EUR')

Now the issue would be that, if we try to fetch a balance like this, we will have currency co-mingled.

ie:

const balance = await myBook.balance({
  account: "Assets:Accounts Receivable",
  client: "Joe Blow"
});

So we would need to have a mandatory currency field here. And filter that out.

I'm wondering what the most efficient way to do it would be?

Simply adding a currency field in transaction, and modifying the balance function? Any other idea?

Failing to get started

Hello,

I've been looking for a double-entry accounting system package to use in my new project, and came across medici.
The project looks interesting and I am trying to start building a POC using it.

However, I cannot find a way to specify the configuration for mongodb to use, nor does it seem to work out of the box (without any specified configuration).

I keep getting the below error, when I try to run the example code:

TransactionError: Failure to save journal: Operation medici_transactions.insertMany() buffering timed out after 10000ms
at Entry.commit in medici/build/Entry.js โ€” line 124

The code I'm using is as below:

const { Book } = require("medici");
const journal = new Book("FY 2022-23");
const amount = 123456.78;
await journal.entry("Sample Entry")
    .debit("Incomes:Sales", amount, {narration: "Debit towards Sample Entry"})
    .credit("Expenses:Purchases", amount, {narration: "Credit towards Sample Entry"})
    .commit();

Could you please help me identify what am I doing wrong here?

FastBalance approaches and solutions

I thought about this issue multiple times, and these were my "simple" solutions.

  1. Account Table
    All the transactions will be summed up in an account document. Potential issue is the writeLocks on heavy used accounts.

  2. Create a balanceSumUp collection (UPD: we have implemented this solution)
    The actual issue of balance is that the operation is O(N) where N is the amount of transactions assigned to an account. So probably the easiest solution would be to make a balance call and store the value into the balanceSumUpTable. So the BalanceSumUp would contain the last transactionId of the balance method, the sum of the balance, and an expiration value, e.g. daily. So what happens is, thay we first search for the balance in the dailyBalanceSumUp Collection. If we find it, we determine the balance and the transaction id we stored. We then do the balance but the _id has to be bigger than the last _id of balanceSumUp. Probably needs an appropriate index. But what happens is that if we have balance of an Account with 1000000 transactions we would not read 1000000 transactions but e.g. the last 1000 of the 1000000 transactions. Thus reducing the Runtime to O(n+1), where n is the amount of additional transactions since the persisted balance. If you set the expires (when the document is automatically deleted by mongo) to 1 day than you would have only once a day the slow down to run the whole balance. Or you set the expires (mongo expires) to 2 days and add a second expires to 1 day, but this expires does not result to a automatically deletion but indicates the freshness of the balance. So you take the last persisted balance, check if it is fresh enough, if so you calculate the balance since then. If it is not fresh, you persist the new balance to the collection, were you update freshness and expires. So you have a potential write conflict when writing to the persisted balance resulting in a retry to persist the balance?! But only once a day.
    Or you don't expire at all and do just the freshness check.

This would not make it necessary to store additional information to the transactions. And it should be still faster than traditional balance.

List of Journal Entries

Hello,

I've been playing around your lib and like it a lot. I was wondering how I would go about adding a method to get the journal entries and populate those with the debits and credits. I have a usecase where I would need to upload the entries to an external accounting system.

I looked into the source and saw the this.journalModel = mongoose.model('Medici_Journal');

and of course have used the ledger() method.

Would I just follow the same style of the ledger method and populate the ID's normally in mongodb?

Kindly advise, I can even put in a PR if this feature is warranted. :)

Commit not working

I am trying to run an example with MongoDB back end.

` const bookDb = new book("MyBook");

let journal = await bookDb
  .entry("Test Entry")
  .debit("Assets:Receivable", 500, { clientId: "12345" })
  .credit("Income:Rent", 500)
  .commit();`

But I don't see the commit working.
Also is there a way to specify the DB name so as to support multi tenant solution.

An working example on MERN stack would be of great help.

Question: Future Posting Date

Hi Medici team et. al., thank you SO MUCH for creating this package - super helpful!! Asking for tips for best approach / recommendation on creating a journal entry with a future posting date.

Consider this scenario: create a journal entry with a future posting date, say 14 days (parameterized) later.

As of today:
#1. Journal and Transaction have datetime and timestamp attributes at the journal entry time.
#2. Book.ledger({start_date: startDate, end_date: endDate}), Book.balance({start_date: startDate, end_date: endDate}).... are based on entry date as well.

What we need is to have extra field so-called posting-date.
On #1-enhanced: Journal and Transactions have extra posting date attributes.
On #2-enhanced: ability to query ledger and balance based on posting date.
#2a. If query as of today ==> query returns no data since posting dates are in the future.
#2b. If query as of 14 days later ==> query returns the journal and transactions since within the posting dates.

Thoughts / tips / help? Thank you in advance!!

Questions on the implementation

First of all, thank you for the project, I'm learning a lot from it. I was interested on implementing the same idea on a SQL database, however I had some doubts.

What makes me wonder is if it makes sense to create a collection (table if SQL) for each account. e.g. a collection for Assets:Cash:USD, one for Assets:Cash:EUR and so on. Was this ever considered on this project? what would be the drawbacks? Because as I see having all on medici_transactions tends to make the collection to get large in size too quickly, and that brings the question of whether this would be an issue -mostly performance-wise- when we query this collection or inspect the data.

demo or sample

I just reviewed this project. It's 5 stars and great! Almost everything is documented but what got me stuck there was a starting point or a startup demo. This would be of great help to others to kick things off immediately.

NIIF Support

Please sayme if this project its compatible with NIIF international norms of IFRS?

Thanks!

Mongoose connection string

I read the document carefully but still can't find the introduction how to provide my mongodb uri to medici package?

Actually, my piece of code below throw the error Failure to save journal: Operation `medici_transactions.insertMany()` buffering timed out after 10000ms

const myBook = new Book("MyBook", { precision: 18 });
const journal = await myBook
  .entry("Received payment")
  .debit("Assets:Cash", 1000)
  .credit("Income", 1000, { client: "Joe Blow" })
  .commit();
const { balance } = await myBook.balance({
    account: "Assets:Accounts Receivable",
    client: "Joe Blow",
  });
console.log("Joe Blow owes me", balance);

Is there any one facing problem like me?

Intent to ship v7.0

Hello @Uzlopak

I am going to publish the new v7.0. The only breaking change - other default indexes. See the latest commits.

Today was a hugely frustrating day for me. Turns out all the default indexes were an absolute horsedump.

After the improvements not only all our ledger queries started to take milliseconds rather than seconds, but also we decreased the index RAM usages by x3 times.

So, this message is mostly FYI. Make sure you're ready for the release.

Voided Docs pair keep unvoid

When voiding a transaction the void pair keeps unvoid, so you could not filter only unvoids.
EXAMPLE:

var Libro = new medici.book('Libro');
Libro.entry("test").debit("Activos:Banco", 5000).credit("Gastos:Proyecto", 5000).commit()
    .then( function (journal) {
        // ill ignore the returned journal
        Libro.ledger("Activos").then( function(docs) {
            // Now ill void the first item
            Libro.void( docs.results[0]._journal, "Error" ).then( function () {
                // ill check the results
                Libro.ledger("Activos").then( function(docs) {
                    console.log(docs);
                });  
            });
        });
});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.