BaryoDev / BaryoDev/barakoCMS

Epic: model a business document by configuration, so invoicing and accounting need no C#

Open
#338 2 comments 0 reactions 1 assignee Claimed by @arnelirobles View on GitHub
enhancement epic
Dominant language
C#
Stars
6
Forks
7
Avg merge
4h 42m
Merged PRs (30d)
307

Description

The connector epic (#325) makes barakoCMS talk to other systems by configuration. This is the other half: modelling the thing being talked about.

The stated goal is that the usual customisation in any project, blog post through invoicing through accounting, is done by configuring rather than coding. Connectors get an invoice *out* to Xero. They do not let anyone define an invoice.

## What a blog post needs that already exists

A blog post is a flat bag of typed fields with a three-state lifecycle, and that is exactly what `ContentTypeDefinition` plus `ContentStatus` provides. `FieldTypeRegistry` accepts text, richtext, markdown, money, decimal, date, json, array, object and more. Workflows trigger on created and updated. For a blog, the configure-not-code promise already holds.

## What an invoice needs that does not exist

An invoice is not a flat bag. Every gap below is load-bearing, and the first one blocks the rest.

### 1. Relations between content types

An invoice points at a customer. A line points at a product. There is no `reference` field type: `FieldDefinition.Type` accepts the scalar set and `FieldTypeRegistry` has no relation entry. The comment in `Models/ContentTypeDefinition.cs:36-39` says reference and blob are planned and cites roadmap F.4/F.6, which no longer exists in `ROADMAP.md`.

Without this there is no way to express any business document, because business documents are relational by definition. It also limits #328: a saved query over a flat type cannot answer "unpaid invoices for this customer".

This is the one that decides whether the rest is worth doing.

### 2. A lifecycle per type, instead of one enum for everything

`ContentStatus` is `Draft`, `Published`, `Archived`, in the core, for every content type. An invoice is Draft, Sent, Paid, Overdue, Void. A journal entry is Draft, Posted, Reversed, and posted entries are immutable.

Two things follow that a blog never needed. Transitions have rules: Paid cannot go back to Draft, and a posted entry cannot be edited. And a transition is the natural workflow trigger for this whole class of work, because "when an invoice becomes Paid, notify accounting" is the sentence people actually say. `TriggerEvent` today is Created or Updated, which cannot express it.

### 3. Computed fields

Quantity times rate, plus tax, equals total. Configure-not-code ends at the first arithmetic otherwise, and every deployment writes the same custom action to add up line items.

This is where scope discipline matters most, for the same reason #328 says a query must not become a query language. An expression field is an injection surface and an unbounded-cost surface, edited by whoever configures a content type. A fixed set of aggregations over a relation, sum, count, min, max, is most of the value at a fraction of the risk.

### 4. Time-based workflow triggers

Workflows fire on content events. Invoicing needs "on the first of the month, raise the recurring invoices" and "three days before due, send a reminder". Neither is a content event, and there is nowhere to hang them.

`ScheduledContentService` already exists and already holds an advisory lock for scheduled publish and unpublish, so the leasing problem is solved. This is a trigger kind, not new infrastructure.

### 5. Gapless numbering

Invoice numbers must be unique, sequential and gapless, and in several jurisdictions that is a legal requirement rather than a preference. Counting existing rows is wrong under concurrency and wrong after a delete. This wants a real sequence, allocated in the same transaction as the document.

Small, unglamorous, and the kind of thing that is discovered in production.

### 6. Inbound webhooks

#325 excludes them deliberately and says so. For accounting the exclusion is the difference between a demo and a system: payment received in Stripe has to come back and mark the invoice paid. Outbound only means every integration is half a loop.

Different trust model, so it wants its own design rather than being bolted onto the connector.

## Order, and why

1 first, because 2 through 5 all assume it and none of them can be demonstrated without it. Then 2, because the lifecycle is what workflows hang off and getting it wrong is a data migration. Then 3 and 4, which are independent of each other. Then 5, which is small. Then 6, which is its own design.

## What can be gated in configuration and what cannot

Mostly not, and it is worth being explicit because config is the right instinct in the wrong place here.

These are capabilities, not policies. A `reference` field type either exists or an invoice cannot be modelled, and a flag that turns it off just makes it absent in a way the schema cannot see. Compare `Erasure:Mode` and `History:Enabled`, where the deployment genuinely has something to decide.

Two exceptions. **6 wants config**, because accepting inbound calls changes a deployment's exposure and an operator should be able to say no. And **2 could carry a strictness flag**, whether an unknown transition is refused or merely recorded, since an existing deployment adopting per-type lifecycles will have data that predates the rules.

## Done when

An operator can define an invoice, relate it to a customer, total its lines, move it through a lifecycle, number it, raise it on a schedule and send it to a third party, without a developer writing C#. That sentence is the acceptance test, and today the only clause that holds is the last one, once #325 lands.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.