Documentation
a. Overview
#This Quickstart will introduce you to the FRAGMENT development cycle.
b. Prerequisites
#- A FRAGMENT Workspace
- Node version 16 or higher installed locally
- Homebrew
c. Steps
#brew tap fragment-dev/tap
brew install fragment-dev/tap/fragment-cli
-
Authenticate your local environment to your Workspace.
fragment login
Create a new API Client. Paste the
Client ID
,Secret Key
,API URL
, andOAuth URL
into the CLI. -
Create your first Schema.
fragment init
This will write an example Schema to
./fragment.jsonc
-
Start the local development environment. This will open a schema visualizer in your browser. The visualizer automatically updates every time the file changes.
fragment start
-
Open a text editor to edit
fragment.jsonc
. On line 14, add a second child Ledger Account toassets-root
.2nd child asset Ledger Account{ "key": "reserve-bank", "name": "Reserve Bank", "children": [] },
-
Post a Ledger Entry from the visualizer to see how balance changes are reflected in your Chart of Accounts.
-
Create a Ledger with your Schema.
fragment store-schema
This stores your Schema in FRAGMENT. Subsequent calls create a new version if the Schema changes.
fragment create-ledger --name "sample ledger" --ik "1"
This creates a Ledger with your Schema. In the dashboard, you should see the created Ledger.
-
Post a Ledger Entry via the API.
fragment add-ledger-entry --ik "entry-ik-1" --ledger.ik "1" --type sell_something --param "sales_before_tax=10000" --param "tax_payable=500"
This posts an entry of type
sell_something
by calling theaddLedgerEntry
mutation. The other mutations that post Ledger Entries arereconcileTx
,createOrder
, andmakeBankTransfer
.
Next:
- Learn more about the FRAGMENT data model, starting with Accounting.
- Get started developing with the API Overview.
a. Workflow
#The FRAGMENT workflow is:
- Ledger: a data structure modeling funds for a single entity. Ledgers are created using the
createLedger
mutation. The FRAGMENT CLI provides a convenient wrapper around this mutation. If your use case requires you to manage multiple Ledgers, call thecreateLedger
mutation directly from your application. - API: data is created in a Ledger via the GraphQL API. Use
addLedgerEntry
to post logical Ledger Entries,reconcileTx
to reconcile transactions from an external system to your Ledger, andcreateOrder
ormakeBankTransfer
to make payments. See Accounting, Reconciliation, and Payments to learn more about the API. - Schema: a JSON-based declarative source of truth for the structure of your Ledger. In your CI or deployment system, use the CLI to run
fragment store-schema
. This creates or updates your Schema like a database migration. When this is run, FRAGMENT automatically migrates all Ledgers using the Schema with the updated Ledger Accounts and Ledger Entry types.
The API is hosted at:
https://api.us-west-2.fragment.dev/graphql
And you can download the GraphQL API schema at:
https://api.us-west-2.fragment.dev/schema.graphql
You can use this schema as input to auto-generate an SDK to use to call the FRAGMENT API. The GraphQL Foundation maintains an updated list of tools. We recommend that you do this as part of your CI, to ensure that you always have the latest schema.
b. No Decimals
#There are only integer amounts in the API. This forces you to use the minor units in the appropriate currency. For USD, a single unit is 1 cent. To represent $1.25, you'd pass in 125.
c. Idempotency
#In financial systems, it's important to achieve exactly-once processing. For example, when debiting a customer's bank account, only one ACH Debit should be made. FRAGMENT's API is idempotent, so your system needs to only handle at-least-once processing when calling the FRAGMENT API.
To achieve idempotency, mutations in the FRAGMENT API require a unique and stable idempotency key (IK
). These let you confidently retry operations without them running twice. If you execute a mutation more than once with the same IK
and variables, FRAGMENT will ignore your request and return the original response with the isIkReplay
flag set to true. IK
s in FRAGMENT:
- are scoped per-mutation. If you send the same
IK
as input to two different mutations, FRAGMENT will execute both. - are also scoped per-ledger. If you send multiple requests with the same
IK
and same mutation operating on a single ledger, only one mutation will execute, and theisIkReplay
flag will be set totrue
in the response. If you send a request with the sameIK
and same mutation that operate separately on two different ledgers, both mutations execute. - are valid for 30 days. If you resend an
IK
after 30 days, it could be executed as a fresh request. - Idempotency Keys are only valid for 1 hour when initiating transfers at Increase through the
makeBankTransfer
orcreateOrder
mutations. This is so we match Increase's idempotency window. All other FRAGMENT mutations support IKs for 30 days.
d. Authentication
#FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. The flow is:
- Get a fresh access token from the token endpoint:
https://auth.fragment.dev/oauth2/token
using your API client's credentials - Use that token in an
Authorization
header with the valueBearer {{access_token}}
when calling the GraphQL API.
The access token expires in 1 hour. We recommend retrieving a new token for each set of calls you're making. You can generate and use multiple tokens at the same time.
You can create and view API Clients from the FRAGMENT dashboard. All API clients have full access to your workspace.
The call to get an access token follows the OAuth 2 spec, so you can use an OAuth2 library that supports the client credentials grant to retrieve the token, or make an HTTP request.
Name | Value |
---|---|
grant_type | client_credentials |
scope | https://api.fragment.dev/* |
client_id | client ID from dashboard |
Your payload should be in the x-www-form-urlencoded
format:
grant_type=client_credentials&scope=https://api.fragment.dev/*&client_id={{client_id}}
Name | Value |
---|---|
Content-Type | application/x-www-form-urlencoded |
Authorization | Basic {{client_credentials}} where client_credentials is the base 64 encoded version of: {{client_id}}:{client_secret}} |
The response will be a JSON Object containing your access token.
{
"access_token": "<access token>",
"expires_in": 3600,
"token_type": "Bearer"
}
Here are some code snippets that show how to hit the https://auth.fragment.dev/oauth2/token
endpoint.
curl --location --request POST 'https://auth.fragment.dev/oauth2/token' \
--header 'Authorization: Basic Mmg0dDBjdjdxdjVjOXIwcWdzMW8zZXJrdWs6c3VwZXJTZWNyZXRDbGllbnRTZWNyZXQ=' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=https://api.fragment.dev/*' \
--data-urlencode 'client_id=2h4t0cv7qv5c9r0qgs1o3erkuk'
import http.client
import b64encode from base64
client_id = '2h4t0cv7qv5c9r0qgs1o3erkuk'
client_secret = 'superSecretClientSecret'
payload = f'grant_type=client_credentials&scope=https%3A%2F%2Fapi.fragment.dev%2F*&client_id={client_id}'
client_credentials = f'{client_id}:{client_secret}'
encoded_creds = (
b64encode(client_credentials.encode('ascii'))
.decode('ascii')
)
conn = http.client.HTTPSConnection("auth.fragment.dev")
headers = {
'Authorization': f'Basic {encoded_creds}',
'Content-Type': 'application/x-www-form-urlencoded',
}
conn.request("POST", "/oauth2/token", payload, headers)
data = conn.getresponse().read()
print(data.decode("utf-8"))
import axios from "axios";
const btoa = (str) => Buffer.from(str).toString("base64");
const clientId = "2h4t0cv7qv5c9r0qgs1o3erkuk";
const secret = "superSecretClientSecret";
const getToken = async () => {
const auth = btoa(`${clientId}:${secret}`);
const data = new URLSearchParams();
data.append("grant_type", "client_credentials");
data.append("scope", "https://api.fragment.dev/*");
data.append("client_id", clientId);
const response = await axios.request({
method: "POST",
url: 'https://auth.fragment.dev/oauth2/token',
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${auth}`,
Accept: "*/*",
},
data,
});
if (!response.data.access_token) {
throw new Error(
"didn't get an access token from auth endpoint"
);
}
return response.data.access_token;
};
e. Match Inputs
#When referencing an entity in FRAGMENT, the API requires you to provide a MatchInputs
. This gives you the flexibility to match for an entity in multiple ways.
Each entity has a FRAGMENT-generated id
which can be used in all queries.
For Ledgers, it's recommended to query with the IK's you've provided to FRAGMENT instead. This lets you not store the FRAGMENT ID of the Ledger in your system.
{
"ledger": {
"ik": "ledger-ik"
}
}
You can also match LedgerAccounts via their path
. The path
is a forward-slash-delimited string containing the IK of a Ledger Account and all its direct ancestors. When querying with path
, you'll also need to provide a LedgerMatchInput to identify which Ledger the Ledger Account belongs to.
{
"path": "parent-ik/child-ik/grandchild-ik",
"ledger": {
"ik": "ledger-ik"
}
}
f. Error Handling
#Errors are first-class citizens of our API. All mutations return a union type that represents either an error or a successful result. For example, the response type of the addLedgerEntry
mutation is as follows:
union AddLedgerEntryResponse = AddLedgerEntryResult | BadRequestError | InternalError
When calling the API, your code should:
- Query the
__typename
field to see if the operation was successful. - Handle non-200 responses. This typically happens due to transient errors or if your query does not conform to FRAGMENT's GraphQL spec (i.e: omitting a required variable or selecting a nonexistent field).
- Handle error type 200 responses. This is always a
BadRequestError
orInternalError
. - Handle success types (i.e.
AddLedgerEntryResult
).
g. Querying
#To read data, FRAGMENT exposes several top level queries. These queries support one or more lookup parameters via *Match
types. These are documented in more detail in the API Reference.
ledgers
List ledgers in a workspace.ledger(ledger: LedgerMatchInput)
Get a Ledger.ledgerAccount(ledgerAccount: LedgerAccountMatchInput)
Get a Ledger Account.ledgerEntry(ledgerEntry: LedgerEntryMatchInput)
Get a Ledger Entry.ledgerLine(ledgerLine: LedgerLineMatchInput)
Get a Ledger Line.order(order: OrderMatchInput)
Get an Order.tx(tx: TxMatchInput)
Get a transaction.
FRAGMENT allows you to query relationships between entities via Connection
types. These expansions are documented alongside the queries in the API Reference.
ledgerAccounts
via Ledger. List Ledger Accounts in a Ledger.ledgerEntries
via Ledger. List Ledger Entries in a Ledger.lines
via LedgerAccount. List Ledger Lines in a Ledger Account.lines
via LedgerEntry. List Ledger Lines in a ledger entry.ledgerLines
via Tx. List Ledger Lines reconciled to a transaction.ledgerEntries
via Tx. List Ledger Entries reconciled to a transaction.
Using these connections, you can construct complex queries over the data graph stored in FRAGMENT.
h. Pagination
#Fields that return lists support cursor-based pagination.
- Results are returned under a
nodes
property as an array. ThepageInfo
property contains cursors pointing to the next page and previous pages - You can send a cursor to the
after
orbefore
arguments on list fields to retrieve a specific page - The
first
argument sets the page size. The default is 20 and the maximum is 200 - Once a page size has been set in the initial request, all subsequent pagination has to use the same page size
Here's an example query that uses pagination to retrieve 2 Ledger Accounts at a time:
query GetLedgerAccounts(
$ledgerId: ID!
$after: String
$first: Int
$before: String
) {
ledger(ledger: { id: $ledgerId }) {
id
name
ledgerAccounts(
after: $after
first: $first
before: $before
) {
nodes {
id
name
type
}
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
}
}
}
{
"ledgerId": "example-ledger-id-1",
"first": 2
}
The response looks like this:
{
"data": {
"ledger": {
"id": "example-ledger-id-1",
"ledgerAccounts": {
"nodes": [
{
"id": "example-ledger-account-id-1",
"name": "Test 1",
"type": "asset"
},
{
"id": "example-ledger-account-id-2",
"name": "Test 2",
"type": "asset"
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "<some-end-cursor>",
"hasPreviousPage": false,
"startCursor": null
}
}
}
}
}
To retrieve the next page, send the same query but with the after
argument:
{
"ledgerId": "ledger-id-1",
"after": <some-end-cursor>
}
The response looks like this:
{
"data": {
"ledger": {
"id": "example-ledger-id-1",
"ledgerAccounts": {
"nodes": [
{
"id": "example-ledger-account-id-3",
"name": "Test 3",
"type": "asset"
},
{
"id": "example-ledger-account-id-4",
"name": "Test 4",
"type": "asset"
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": null,
"hasPreviousPage": true,
"startCursor": "<some-start-cursor>"
}
}
}
}
}
To retrieve the previous page of results, send the same query but with the before
argument:
{
"ledgerId": "ledger-id-1",
"before": <some-start-cursor>
}
The response will be the first page of results.
i. Filtering
#You can filter result sets via the filter
parameter on Connection
types when performing list queries. The API Reference documents this in detail.
The API has several filter types built-in:
TypeFilters
(e.g.LedgerAccountTypeFilter
orTxTypeFilter
)DateFilter
DateTimeFilter
ExternalAccountFilter
LedgerAccountFilter
To filter by the type on an Entity, send a filter argument in your query with an
equalTo
operator:
query GetLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: { id: $ledgerId}) {
ledgerAccounts(filter: $filter) {
nodes {
id
name
type
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"type": {
"equalTo": "asset"
}
}
}
To search for multiple types, you can use the in
operator:
{
"filter": {
"type": {
"in": ["asset", "liability"]
}
}
}
DateFilters
work similarly to TypeFilters
and allow you to query for a specific date:
query GetAccountLinesOnDate($ledgerAccountId: ID!, $filter: LedgerLinesFilterSet) {
ledgerAccount(ledgerAccount: {id: $ledgerAccountId}) {
lines(filter: $filter) {
nodes {
id
posted
date
amount
description
ledgerEntryId
}
}
}
}
{
"ledgerAccountId": <ledger-account-id>,
"filter": {
"date": {
"equalTo": "1969-07-21"
}
}
}
To specify a set of dates:
{
"filter": {
"date": {
"in": ["1969-07-20", "1969-07-21"]
}
}
}
To search for a date range use a DateTimeFilter
You can filter with ISO 8601 timestamps as a filter.
To find entities that were created or posted before a certain timestamp use the before
operator:
query GetLedgerEntries($ledgerAccountId: ID!, $filter: LedgerLinesFilterSet) {
ledgerAccount(ledgerAccount: {id: $ledgerAccountId}) {
lines(filter: $filter) {
nodes {
id
posted
date
amount
description
ledgerEntryId
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"posted": {
"before": "1969-07-21T02:56:00.000Z"
}
}
}
You can also search using the after
operator:
{
"filter": {
"posted": {
"after": "1969-07-21T02:56:00.000Z"
}
}
}
To specify a range supply both before
and after
. You can also use shortened dates:
{
"filter": {
"posted": {
"after": "1969-01-01"
"before": "1969-12-31"
}
}
}
To filter Ledger Accounts by their linked accounts, send a filter argument in your query with an equalTo
, in
, or isLinkedAccount
operator:
query GetLinkedLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: {id: $ledgerId}) {
id
ledgerAccounts(filter: $filter) {
nodes {
id
name
linkedAccount {
id
name
}
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"isLinkedAccount": true
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"linkedAccount": {
"equalTo": {
"id": <external account id>
}
}
}
}
To search for multiple parents you can use the in
operator:
{
"ledgerId": "ledger-id-1",
"filter": {
"linkedAccount": {
"in": [
{ "id": <external account id 1> },
{ "id": <external account id 2> }
]
}
}
}
To filter Ledger Accounts by the presence of a parent or a specific parent, send a filter argument in your query with an equalTo
, in
, or hasParentLedgerAccount
operator:
query GetLedgerAccounts($ledgerId: ID!, $filter: LedgerAccountsFilterSet!) {
ledger(ledger: {id: $ledgerId}) {
id
ledgerAccounts(filter: $filter) {
nodes {
id
name
parentLedgerAccountId
parentLedgerAccount {
id
name
}
}
}
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"hasParentLedgerAccount": true
}
}
{
"ledgerId": "ledger-id-1",
"filter": {
"parentLedgerAccount": {
"equalTo": {
"id": "<Ledger Account id>"
}
}
}
}
To search across multiple parents you can use the in
operator:
{
"ledgerId": "ledger-id-1",
"filter": {
"parentLedgerAccount": {
"in": [{ "id": "<parent ledger id 1>" }, { "id": "<parent ledger id 2>" }]
}
}
}
You can combine filters by adding multiple components to the filter block. Results are ANDed. Example:
{
"ledgerId": "ledger-id-1",
"filter": {
"hasParentLedgerAccount": true,
"type": {
"in": ["asset", "liability"]
}
}
}
a. Ledgers
#A Ledger is a data structure that implements double entry accounting. Ledgers have a perspective; they track the money movements for a single business entity. In FRAGMENT, you model your Ledger using a Schema.
b. Ledger Accounts
#The core primitive in a Ledger is the Ledger Account. A Ledger Account tracks movements of money. Each Ledger Account encodes the entity's relationship with other entities such as their bank, customers, and vendors.
{
"chartOfAccounts": {
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset",
},
{
"key": "debt-root",
"name": "Debt",
"type": "liability",
},
]
}
}
Within a schema, chartOfAccounts
models the Ledger Accounts that will be created in your Ledger. The above Schema would create two Ledger Accounts: assets-root
and debt-root
.
A Ledger Line records a single change to a Ledger Account. The balance of a Ledger Account is the sum of Ledger Lines posted to it.
Ledger Accounts can be split into two layers, State and Change.
Within the State and Change layer, Ledger Accounts have specific types. These types map directly to common financial reports like Balance Sheets and Income Statements.
State
asset Assets: what you own
liability Liabilities: what you owe
Change
income Income: what you've earned
expense Expense: what you've spent
{
"chartOfAccounts": {
"defaultCurrencyMode": "single",
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset"
}
]
},
}
In a Ledger Account Schema, use the type
field to set the Ledger Account's type. A Ledger Account can only have one type and it cannot change once the Ledger Account is created.
In a double-entry Ledger, the total balance of all Ledger Accounts in the State layer equals the total balance of all Ledger Accounts in the Change layer.
- The State layer tracks the entity's net worth. The total of all the Ledger Account balances in the State layer adds up to the current net worth of the entity.
- The Change layer tracks updates to net worth. When the total net worth changes, Ledger Accounts in the Change layer track the reason for that update.
This principle is captured in FRAGMENT's accounting equation:
By design, FRAGMENT doesn't provide an Equity account type. Liability and Equity are equivalent in the Accounting Equation, so having both types would be redundant. You can model Equity on your Ledger as a Ledger Account with type Liability. The sample Schema in the Quickstart demonstrates this.
c. Ledger Entries
#A Ledger Entry is a single update to a Ledger. A Ledger Entry consists of two or more Ledger Lines. The sum of the amounts of the Ledger Lines must follow the Accounting Equation.
This is a simple Ledger Entry containing two Ledger Lines. One Ledger Line posts to an Asset account and the other posts to an Income account.
{
"type": "buy_materials",
"lines": [
{
"key": "credit_bank_for_materials",
"account": { "path": "assets-root/bank" },
"amount": "-{{materials_cost}}"
},
{
"key": "debit_materials_expense",
"account": { "path": "expense-root/materials" },
"amount": "{{materials_cost}}"
}
]
}
A Ledger Entry is configured with a type
and defines the set of Ledger Lines that it creates. The amounts are parameterized {{variables}}
that you must provide to the API at runtime.
{
"type": "buy_materials",
"ledger": {
"ik": "my-ledger-ik"
},
"parameters": {
"materials_cost": "1500"
}
}
To post this Ledger Entry, use the LedgerEntryInput
type. This type is shared across all the mutations that post Ledger Entries: addLedgerEntry(), reconcileTx(), makeBankTransfer(), and createOrder(). The CLI also provides fragment add-ledger-entry
, a convenient wrapper around addLedgerEntry
.
{
"type": "sell_something",
"lines": [
{
"key": "debit_bank_from_sales",
"account": { "path": "assets-root/bank" },
"amount": "{{sales_before_tax}} + {{tax_payable}}"
},
{
"key": "income_from_sales_before_tax",
"account": { "path": "income-root/sales" },
"amount": "{{sales_before_tax}}"
},
{
"key": "tax_payable",
"account": { "path": "debt-root/tax_payables" },
"amount": "{{tax_payable}}
]
}
For more complex Ledger Entries, you can encode arithmetic in the Schema to configure Ledger Line amounts. Currently, only +
and -
are supported.
d. Chart of Accounts
#The Chart of Accounts is a data structure that models the structure of your Ledger and organizes the complexities in your flow of funds.
In FRAGMENT, Ledger Accounts exist in a tree hierarchy. This enables you to incorporate the dimensionality of your data directly into the Ledger. You will typically record data at the leaf nodes, such that their balances will propagate up the tree.
{
"chartOfAccounts": {
"accounts": [
{
"key": "Customer 1",
"type": "liability",
"children": [
{
"key": "Available"
},
{
"key": "Unsettled",
},
{
"key": "Blocked"
},
]
}
]
}
The Chart of Accounts lets you encode this hierarchy using the children
field. You might use it to maintain multiple balances for a given user of your product, representing the states their funds are in. The hierarchy goes up to 10 levels deep.
The Chart of Accounts can also include Ledger Account templates. A template is a repeated subtree of Ledger Accounts that are configured in the Schema. Templates can be useful when your Ledger models financial relationships with an multiple semantically similar entities, such as users of your product.
{
"chartOfAccounts": {
"accounts": [
{
"key": "Customer",
"type": "liability",
"template": true,
"children": [
{
"key": "Available"
},
{
"key": "Unsettled",
},
{
"key": "Blocked"
},
]
}
]
}
The template
field in the Schema signifies a Ledger Account template. When the Schema is applied, this subtree is not instantiated. Instead, accounts in this subtree are created just-in-time when you first post a Ledger Entry to this part of the tree.
{
"type": "credit_user",
"lines": [
{
"key": "funds_arrive_in_bank",
"account": {
"path": "assets-root/bank"
},
"amount": "-{{funding_amount}}"
},
{
"key": "credit_user_balance",
"account": {
"path": "liabilities-root/user:{{user_id}}/available"
},
"amount": "{{funding_amount}}"
}
]
}
Ledger Entries that posts to Ledger Account templates specify an identifier within the Ledger Account path
. When the Ledger Entry is posted, the path is constructed with the parameters
provided to the API. The Ledger Account subtree gets created if necessary.
In this example, the path
for the available
account for user 1234
would be liabilities-root/user:1234/available
.
The templated path
syntax lets you use identifiers from your system to refer to Ledger Accounts in FRAGMENT. It is also used when querying.
e. Currencies
#Ledger Accounts in FRAGMENT support the single
or multi
currency mode. A Ledger Account in the single
currency mode only accepts Ledger Lines of the same currency as the Ledger Account. In the multi
currency mode, Ledger Lines of any currency are allowed.
FRAGMENT natively supports all ISO-4217 currencies in addition to a list of common cryptocurrencies. You can also add your own custom currencies to build brokerages, track rewards points, or ensure pre-tax and post-tax money don't mingle.
{
"chartOfAccounts": {
"defaultCurrency": {
"code": "USD"
},
"defaultCurrencyMode": "single",
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset"
}
]
}
}
In most use cases, all Ledger Accounts in a Ledger will share the same currency
and currencyMode
. For convenience, you can set these as defaults within Schema.
{
"chartOfAccounts": {
"defaultCurrency": {
"code": "USD"
},
"defaultCurrencyMode": "single",
"accounts": [
{
"key": "assets-root",
"name": "Assets",
"type": "asset",
"currency": {
"code": "CAD"
}
}
]
}
}
To override the default for a specific Ledger Account, you can set currency
and currencyMode
directly on that account.
{
"type": "buy_materials",
"lines": [
{
"key": "credit_bank_for_materials",
"account": { "path": "assets-root/bank" },
"amount": "-{{materials_cost}}",
"currency": {
"code": "{{expense_currency}}"
}
},
{
"key": "debit_materials_expense",
"account": { "path": "expense-root/materials" },
"amount": "{{materials_cost}}",
"currency": {
"code": "{{expense_currency}}"
}
}
]
}
You can also parameterize the currency for a Ledger Line. This is required when posting Ledger Entries to Ledger Accounts with the multi
currency mode. For more on how you might use the multi
currency mode, see Multi-currency.
f. Logical Clock
#In Accounting, Ledger Entries are recorded at the time the money movement event happened. However, since there is latency in software systems, you can't post a Ledger Entry exactly when the corresponding event happens. To solve this, Ledgers use a logical clock.
Ledger Entries have two time timestamps:
posted
: the logical time for the Ledger Entrycreated
: the system time the Ledger Entry was recorded in FRAGMENT
You provide posted
in LedgerEntryInput when you call the API. FRAGMENT automatically generates created
based off internal system time.
You can post Ledger Entries at any point in time, including the future. You can also query balances at any timestamp. Posting a Ledger Entry will update all balances from the posted
time into the future.
g. Balances
#FRAGMENT supports querying a Ledger Account for its balances, historical balances, and balance changes.
Each Ledger Account has six balances that can be queried:
ownBalance
The sum of all Ledger Lines in the Ledger Account, excluding Ledger Lines in child Ledger Accounts.childBalance
The sum of all Ledger Lines in child Ledger Accounts in the same currency as this Ledger Account.balance
The sum of all Ledger Lines, including child Ledger Accounts in the same currency as this Ledger Account.ownBalances
The sum of all Ledger Lines in the Ledger Account in all currencies, excluding Ledger Lines in child Ledger Accounts.childBalances
The sum of all Ledger Lines in child Ledger Accounts in all currencies.balances
The sum of all Ledger Lines, including child Ledger Accounts in all currencies.
query GetBalances ($ledgerId: ID!) {
ledger(ledger: {id: $ledgerId}) {
ledgerAccounts {
nodes {
id
name
type
ownBalance
childBalance
balance
childBalances {
nodes {
currency {
code
}
amount
}
}
balances {
nodes {
currency {
code
}
amount
}
}
}
}
}
}
For a Ledger with a single currency you will only use balance
and childBalance
. When tracking multiple currencies in the same Ledger, use balances
and childBalances
.