Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: added a deprecated

Table of Contents
maxLevel2

...

This approach keeps the payment "simple" and consistent with the other transaction APIs.  The storage layer performs some aggregation in the database of payments/credits based on the sourceInvoiceId specified in each payment.  The flow looks something like this:

Introduce new tables: 

  • invoice_transaction_summary - Contains an invoiceId and the total number of transactions (payments/credits) expected when "paying" this invoice.
  • temp_invoice_payments - Temporary storage of payments until all payments related to the invoice are received.

When an invoice is paid

  • mod-invoice calls POST /finance/invoice-transaction-summary 
    • mod-finance calls mod-finance-storage to create the entry if one doesn't already exist
  • mod-invoice generates payments and calls POST /finance/payments
    • mod-finance checks calls POST /finance-storage/payments

When an payment is received, 

  • Persist the payment record in a temp_invoice_payments table
  • Count the number of records in the temp_invoice_payments table.  If this is the last payment, (the number of entries in the temp table == numTransactions)
    • In a database transaction:
      • Apply the payments/credits from the temp_invoice_payments table and perform the summary # updates (e.g. update budget, ledger, encumbrances, etc. - unavailable/available/encumbered/expended/etc.)
        • If everything works, cleanup the temp_invoice_payments table and return 200 OK
        • If something fails, no actual records are updated.  The payment API call can fail and can be retried (POST payment/credit are idempotent)

When an "approved" invoice is removed:

  • mod-invoice makes a call to mod-finance to ensure that the invoice_transaction_summary has been cleaned up.  These records are small so even if on is orphaned (which shouldn't happen often if ever) it's not a huge deal. 
  • DELETE /finance-storage/invoice-transaction-summary/<invoiceId>

If a payment request fails

  • mod-finance can safely retry it.
  • After N failed tries an error can be returned to mod-invoice and/or the client who can later try to pay the invoice again once the (network?) problem is resolved.

...

  • if awaitingPayment.encumbranceId specified
    • ALWAYS DO THIS PART:
      • Update Budget identified by the transaction fiscal year (fiscalYearId) and the source fund (fromFundId)
        • encumbered decreases by the transaction.amount
        • awaitingPayment increases by the same amount
      • update related encumbrance:
        • transaction(encumbrance).amount is updated to (transaction(encumbrance.)amount - transaction(pending payment).amount)
      • if pendingPayment.amout > encumbrance.amount
        • Update the budget identified by the transaction(encumbrance) fiscal year (fiscalYearId) and the source fund (fromFundId)
          • available decreases by the  pendingPayment.amout - encumbrance.amount
          • unavailable increases by the pendingPayment.amout - encumbrance.amount  (min 0)
          • recalculate overExpended?
    • IN ADDITION TO ABOVE, DO THIS PART IF ENCUMBRANCE IS RELEASED:
      • If encumbrance.status = Released 
        • Update the related encumbrance
          • transaction.amount becomes 0 (save the original value for updating the budget)
          • Update the budget identified by the transaction(encumbrance) fiscal year (fiscalYearId) and the source fund (fromFundId)
            • encumbered decreases by the amount being released
            • available increases by the amount being released
            • unavailable decreases by the amount being released (min 0)
          • Update LedgerFY identified by the transactions fiscal year (fiscalYearId) and source fund (fromFundId)
            • available increases by the transaction amount (min 0)
            • unavailable decreases by the transaction amount
  • if awaitingPayment.encumbranceId is empty
    • Update budget identified by the transaction fiscal year (fiscalYearId) and the source fund (fromFundId)
      • available decreases by transaction amount (min 0)
      • unavailable increases by the transaction amount
      • awaitingPayment increases by the transaction.amount
    • Update LedgerFY identified by the transactions fiscal year (fiscalYearId) and destination fund (toFundId)

      • available increases by the transaction amount 
      • unavailable decreases by the transaction amount (min 0)

...

Method

Path

Request

Response

Description

Interface

Notes

POST/finance/ledgersledgerledgerCreate a ledgerfinance.ledgers
GET/finance/ledgers/<id>Fiscal Year Query (opt)ledgerGET ledger by Idfinance.ledgers

If FY specified:Queries GET /finance-storage/ledger-fiscal-year

  • Return Ledger w/ summary #s
  • Enforces acquisition units based on ledger's assignments, ignores budget's acq. units.

else

  • return ledger w/o summary #s

The fiscalYear query arg is the UUID of a fiscal-year record

GET/finance/ledgers

CQL Query

Fiscal Year Query (opt)

collection<ledger>Search/List ledgersfinance.ledgers

If FY specified:

  • Queries GET /finance-storage/ledger-fiscal-year
  • Return Ledgers w/ summary #s
  • Enforces acquisition units based on ledger's assignments, ignores budget's acq. units.

else

  • return ledger w/o summary #s

The fiscalYear query arg is the UUID of a fiscal-year record

PUT/finance/ledgers/<id>ledgerledgerUpdate a ledgerfinance.ledgers
DELETE/finance/ledgers/<id>NA204Delete a ledgerfinance.ledgers
GET/finance/ledgers/<id>/budgets

CQL Query,

Fiscal Year Query (req)

collection<budget>Get budgets for a ledgerfinance.ledgers

Queries GET /finance-storage/group-fund-fiscal-years (by fiscalYear, ledger, ledger.acqUnits)

Enforces acquisition units based on ledger's assignments, ignores budget's acq. units.

The fiscalYear query arg is the UUID of a fiscal-year record

GET/finance/ledgers/<id>/groups

CQL Query,

Fiscal Year Query (req)

collection<group_fy_summary>Get groups for a ledgerfinance.ledgers

Queries GET /finance-storage/budgets (by fiscalYearId, ledgerId, ledger.acqUnits)

Queries GET /finance-storage/group-fund-fiscal-years (by ledgerId, fiscalYearId, ledger.acqUnits)

  • pages all results or gets them all in one go
  • sums #s for each group
  • returns array of group_fund_fy

Enforces acquisition units based on ledger's assignments, ignores group's acq. units.

Does not support paging

The fiscalYear query arg is the UUID of a fiscal-year record 

GET/finance/ledgers/<id>/current-fiscal-yearNAfiscal_yearGet the current fiscal year for a given ledger record with <id>finance.ledgers

NOTE: Removing GET /finance/ledgers/<id>/budgets.  This functionality will be provided by GET /finance/budgets?query=fiscalYearId=ABC and ledger.id=DEF and ledger.acqUnits=\"XYZ\"

...

As you can see above, there is not API for directly manipulating the LedgerFY records.  These records are automatically updated by the storage module when related changes are made.

  • When a Ledger is created,
    • create a LedgerFY record for the FY identified by ledger.fiscalYearOneId
    • create a fiscalYear for the next period in the fiscalYear series (TODO: more details)
    • create a LedgerFY record for the FY just created
  • When a FiscalYear is created, create a new LedgerFY record for each existing Ledger.(only if fiscalYear endDate > now)  Handle this during rollover...

The approach we're taking will result in LedgerFY records being creating which may never be used - this is especially true in cases where there are simultaneous or overlapping fiscal years, e.g. Texas Fiscal Year (TFY19) and Qatar Fisal Year (QFY19) might both have similar or overlapping periods, but apply to two separate campuses, each with their own finances.  A given ledger would only really apply to one or the other, but LedgerFY records would be created for both.  We need to do this to support rollover.  The LedgerFY records themselves are quite small, so it shouldn't be too much of a problem.

For now we don't have to worry about managing LedgerFY records upon update or delete of a ledger or FiscalYear.  The plan is to implement a soft delete on both so reference integrity should be maintained without intervention.

...

PropertyTypeDefaultRequiredUpdated On TransactionNotesRequirements tickets
idstring
NNUUID - system generated if not specified
namestring
YN= <fund.code>-<fiscal_year.code>, eg. AFRICAHIST-FY19.  Unique
budgetStatusstring
YNenum: Active, Frozen, Planned, Closed
allowableEncumbrancenumber
NNpercentage
allowableExpenditurenumber
NNpercentage
initialAllocationnumber
NYFirst allocation transaction amount
allocationTonumber
NYTotal of allocations to this budget
allocationFromnumber
NYTotal of allocations from this budget
allocatednumber0NY

Running total of all the allocation transactions against the budget.

= initialAllocation + allocationTo - allocationFrom


totalFundingnumber
NY= allocated + netTransfers
cashBalancenumber
NY= totalFunding - expenditures
awaitingPaymentnumber
NY (encumbrance only)Running total of all the invoiced amounts (i.e. waiting to be paid)
availablenumber0NY

= max(totalFunding - unavailable)

NOTE: Doesn't include allowableEncumbered amount


encumberednumber
NY (encumbrance only)Running total of money set aside for purchases
expendituresnumber0NY (encumbrance only)Running total of all the payments recorded by the fund
unavailablenumber
NY

= encumbered + awaitingPayment + expenditures

NOTE: Doesn't include overEncumbered amount


overEncumbrancenumber
NY (encumbrance only)Amount the budget is over encumbered.  i.e. = max(encumbered - max(max(totalFunding - expenditures, 0) - awaytingPayment, 0), 0)
overExpendednumber
NY (encumbrance only)Amount the budget is over expended. i.e. overExpended = max(awaytingPayment + expenditures - totalFunding, 0)
netTransfersnumber0NYThis would actually be calculated by summing all the "Transfers" on this budget.

Jira Legacy
serverFOLIO Issue TrackerSystem Jira
serverId6ccf3fe401505d01-3301b853-368a3c2e-983e90f1-20c466b11a49ee9b165564fc
keyUIF-215

fundIdstring
YNUUID of fund
fiscalYearIdstring
YNUUID of fiscal year
ledgerIdstring
YNUUID of ledger
acquisitionUnitsarray<string>
NNArray of UUIDs corresponding to the acquisition units assigned to this budget.
tagstags
NNinherited from fund
metadatametadata
NYSystem generated metadata (createdBy/updatedBy/createdOn/etc.)

...

PropertyTypeDefaultRequiredUniqueNotesRequirements tickets
idstring
NYUUID - system generated if not specified

Jira Legacy
serverFOLIO Issue TrackerSystem Jira
serverId6ccf3fe401505d01-3301b853-368a3c2e-983e90f1-20c466b11a49ee9b165564fc
keyUIF-211

namestring
YY
codestring
NY
externalAccountNumberExtstring
NN

...

MethodPathRequestResponseDescriptionInterfaceNotes
POST/finance/expense-classes

expense-class

expense_class

Create a expense_classfinance.expense_class


Jira Legacy
serverFOLIO Issue TrackerSystem Jira
serverId6ccf3fe401505d01-3301b853-368a3c2e-983e90f1-20c466b11a49ee9b165564fc
keyUIF-211




GET/finance/expense-classes/<id>NAexpense_classGET expense_class by Idfinance.expense_class
GET/finance/expense-classesCQL Querycollection<expense_class>Search/List expense_classfinance.expense_class
PUT/finance/expense-classes/<id>expense_classexpense_classUpdate a expense_classfinance.expense_class
DELETE/finance/expense-classes/<id>NA204Delete a expense_classfinance.expense_class

...

MethodPathRequestResponseDescriptionInterfaceNotes
POST/finance-storage/expense-classesexpense_classexpense_classCreate a expense_classfinance-storage.expense_classes

Jira Legacy
serverFOLIO Issue TrackerSystem Jira
serverId6ccf3fe401505d01-3301b853-368a3c2e-983e90f1-20c466b11a49ee9b165564fc
keyUIF-211





GET/finance-storage/expense-classes/<id>NAexpense_classGET expense_class by Idfinance-storage.expense_classes
GET/finance-storage/expense-classesCQL Querycollection<expense_class>Search/List expense_classfinance-storage.expense_classes
PUT/finance-storage/expense-classes/<id>expense_classexpense_classUpdate a expense_classfinance-storage.expense_classes
DELETE/finance-storage/expense-classes/<id>NA204Delete a expense_classfinance-storage.expense_classes

...

N.B. This was written prior to 

Jira Legacy
serverFOLIO Issue TrackerSystem Jira
serverId6ccf3fe401505d01-3301b853-368a3c2e-983e90f1-20c466b11a49ee9b165564fc
keyRMB-395
.  These views are no longer needed in light of this new cross-index subquery functionality.  I'm leaving this section as-is in case this approach does not prove adequate for our needs.

...

  • The set of columns to show in search results has not been reviewed by the small group yet.  That could potentially affect this if there are changes.
  • How to express allocateTo/allocateFrom "all" funds?  none?  groups?
    • It was discussed that we might at some point add separate fields for specifying groups of funds that a fund can allocatedTo/From.  At the moment this is the best idea we've come up with.  One potential downside is that it might encourage proliferation of groups which will have a negative impact on performance in the current design due to needing to perform group calculations on the fly.
  • How to capture changes in currency?  Historically?  
    • No need to track the currency code in each fund, but when the fiscalYear ends, we should capture the system currency in use at the time.
    • Displaying search results with different currencies.... e.g. FY18 is in USD, FY19 is in CAD - Is this possible?  Something like:
      • Discussed with the FE guys, and this shouldn't be a problem. 

Appendix

Preventing Partially Paid Invoices (deprecated method)

When an invoice is paid, numerous transactions (payments, credits) will be generated.  Either all or none of these need to be successfully processed; we can't have partially paid invoices.  This presents a challenge of scale.  There could feasibly be hundreds of payments for an invoice.

...

This approach keeps the payment "simple" and consistent with the other transaction APIs.  The storage layer performs some aggregation in the database of payments/credits based on the sourceInvoiceId specified in each payment.  The flow looks something like this:

Introduce a new type of record... let's just call it a "saga" for now - what's proposed here isn't strictly a saga, but it's similar in ways.  Maybe a "paymentManifest" or something is a better name;  anyway - its not important right now.

Saga:

  • invoiceId (UUID of the invoice these payments/credits are related to)
  • numTransactions (int - total number of payments/credits)A payment or credit is posted

Flow:

  • Using the sourceInvoiceId, get the invoiceLines and sum up the total number of fundDistributions - this is numTransactions.
  • Get the saga using payments sourceInvoiceId.  If one does exist, create one.
  • Update the saga record and persist the payment record in a temporary table - do all this in a database transaction
  • If this is the last payment in the saga, (the number of entries in the temp table == numTransactions) - NOTE: we'll probably want to use a unique_index on the id field or something to ensure the same payment isn't being counted more than once.
    • Persist the payments/credits from the temporary table and perform the summary # updates (e.g. update budget, ledger, encumbrances, etc. - unavailable/available/encumbered/expended/etc.) - do this in a database transaction
      • If everything works, cleanup the temporary table and return 200 OK
      • If something fails, no actual records are updated.  The payment API call can fail and can be retried (POST payment/credit are idempotent)

NOTE:  we might be able to move some of this logic to the BL module.  The trade-off there is that the storage module would contain less logic, but would have a more complicated API.

Pros/Cons:

  • Might be able to do some incremental processing instead of having to do it all at once
  • Multiple single payments can be sent - avoids the massive request payload
  • Individual payments/credits can be repeated if needed (the POST payment API is idempotent)

...