The worker wakes on 2 October. September's estimated operating expense still needs its original journal, and the configured reversal was due a day ago. Posting only the reversal would invent an original. Posting both again after a timeout would duplicate them. Moving the late original into October would change the accounting period without authority.
A useful scheduler makes those distinctions visible. Operations can see what remains due and what blocks it. An engineer can preserve the original financial instruction across late execution, retry and changed current policy. We will calculate real month boundaries, create a fixed estimate and its exact inverse, then catch up safely one accepted instruction at a time.
This tutorial uses synthetic Cedar data and an authored UTC monthly policy. It builds on Double-Entry Posting Engine and Correcting and Reversing Entry Algorithm. The result is a portable reference model; actual durable posting is an integration boundary that we will keep explicit.
Choose the obligation before choosing the timer
Our supplied estimate is 25000 USD minor units, or $250. At September's last millisecond, the chosen policy accrues an operating expense:
| Account | Debit | Credit |
|---|---|---|
| operating_accrual_expense_usd | 25000 | 0 |
| operating_accrual_payable_usd | 0 | 25000 |
At the next month's first midnight, the selected policy reverses those exact ordered legs:
| Account | Debit | Credit |
|---|---|---|
| operating_accrual_expense_usd | 0 | 25000 |
| operating_accrual_payable_usd | 25000 | 0 |
The two journals net to zero for this estimate pair. That does not establish that the actual economic expense is zero. A later invoice, recognition adjustment or cash settlement is outside this model. This is also separate from the customer interest calculation and payable-credit workflows earlier in the domain.
Oracle documents configured reversal methods and distinguishes creating a reversal from posting it. That supplies operational context, while our fixed estimate, UTC calendar and exact API remain authored choices. Reversal is not presented as a universal accounting requirement. Oracle journal reversals, accrual reversal examples.
Calculate the actual calendar boundary
For original month p, let F(p+1) be the next month's first midnight UTC. Then accrual_due=F(p+1)−1millisecond and reversal_due=F(p+1). September2026 therefore gives30 September at 23:59:59.999Z and1 October at 00:00:00.000Z. February 2024 uses29 days; February 2100 uses28. The selected profile has no weekend roll, local timezone or fixed30-day shortcut.
The original booking period is September. The inverse booking period is October. Their processing time can be 2 October without changing those facts. On the separate CB-CLOSE-2026/CB-A branch, September starts OPEN so we can demonstrate catch-up. The existing CB-GENERAL branch's policy is unchanged.
Keep four times separate
Template available_at records when the source version became available. Its effective_period says which months it governs. Proposal source_available_at records the host observing this generated command. The sequential result's recorded_at records processing now. A physical database COMMIT and the observer's later committed-export receipt require real integration evidence.
The proposal may be generated at12:00 and accepted at12:01. Requiring source_available_at to equal the new current time would reject a valid delayed proposal. Instead enforce applicable source availability≤proposal observation≤processing now. For an inverse, proposal observation must also follow the accepted original's recording watermark.
The independent template head declares coverage and revision count. Validate the complete history before selecting versions visible at now. An unavailable future version must not enter diagnostics, but malformed future payloads still reject. A version must be announced no later than its effective month's first midnight. A late, backdated change needs a different correction policy; this reference will not rewrite due obligations with hindsight.
An instruction needs a stable identity
The key is the structured tuple of ledger, entity, schedule, original month and kind. Kind is accrual or reversal. The caller's journal_id and business_operation_id are separately bound fields in the complete command. A transport retry preserves every field, including expected_version and the proposal observation time.
The processor authorizes scope and reconstructs accepted state first. If that instruction key already exists, an exact command returns the stored complete result. Any changed field conflicts. Current closed accounts, missing source, clock or capacity cannot revoke an earlier exact accepted result. Authorization still runs before replay so a stored result is not disclosed to another scope.
For new work, current source, account roles, open booking period, schedule version and capacity must all pass. Reusing a caller journal or business identity for a different instruction rejects. A failed modeled attempt adds neither record nor partial identity reservation.
Catch up in dependency order
At 12:00Z on 2 October both September instruction times have passed. The initial plan has two pending obligations: original ready, inverse blocked ORIGINAL_NOT_ACCEPTED. Accepting the original appends one modeled record. Replanning makes the inverse ready. Its amount, accounts, template identity and leg order come from the stored original, never a fresh estimate. Accept it and the next plan is empty for that requested original month.
This dependency order handles partial progress. If the inverse fails after the original succeeds, the original remains accepted. Retrying it returns the same result; it does not add another estimate. The next successful inverse completes the pair.
Closing September before the late original is accepted produces BOOKING_PERIOD_CLOSED. The scheduler cannot solve that by changing booking_period to October. A governed close/reopen/adjustment policy must decide the remedy. Period-Lock Control coordinates that authority and the actual posting barrier.
Cancellation changes future obligations
A cancellation announced25 October and effectiveNovember prevents new November accruals. It does not remove a missing inverse of an accepted September estimate. A later active version can resume future months. Versions are immutable and their effective months strictly increase.
For new planning, every accepted original's template identity must still occur with the same accepted amount/account effect facts. Missing identity makes the new plan indeterminate; changed effect facts reject. The accepted model state does not cryptographically pin every original source metadata field, so the host still owns source immutability. Exact replay returns before current source validation.
Run the reference and inspect the whole result
The canonical package provides both pure APIs and complete fixtures. From its folder, run:
python examples/run_example.py
The example executes original, stored exact inverse and exact replay and checks each entire expected result. The main APIs are:
plan = plan_due_accruals(state, request, context)
result = process_scheduled_accrual(state, command, request, context)
A processor result contains status, code, result and state. The result's journal is a full strict posting command. Its version is the number of records in this one schedule, not a global ledger sequence. committed means accepted in this sequential model. Rejected/indeterminate work returns null result and unchanged state. All output branches are detached, so altering the returned display journal cannot rewrite the accepted audit branch.
The browser lab runs the compiled TypeScript reference. Its complete month list includes all obligations and their status; it does not discard quiet or blocked rows to make a smoother demonstration. Choose the24-month branch to process48 instructions across25 booking months. Change the amount to recalculate the supplied synthetic estimate, then inspect exact minor-unit legs. The lab posts nothing to a database.
Test what a late worker can get wrong
The shared matrix contains48 full transition results and10 full planner results. Seven Python suites also cover independently prescribed calendar boundaries, complete state reconstruction, input/output isolation, cancellation order, proposal/processing clock advance and a full24-month48-instruction loop. Strict TypeScript must match the entire expected JSON, including failures and unchanged state.
Useful adversarial questions include: What happens one millisecond before due? Can a closed prior month be relabeled? Does changed amount under an accepted key conflict? Does exact replay survive a missing current source? Can a cancellation erase an old inverse? Can a malformed future version escape validation? Does48-record capacity still allow an exact replay? Every answer needs an expected state, not just a truthy success flag.
Connect modeled progress to a real close
To use this scheduler in a durable service, atomically bind the stable instruction key and exact payload to Stage1 posting under the period controller. A posting failure rolls back both. Export the resulting committed journal identity and receipt. Reconcile each required due instruction to those actual journal facts before registering a scheduler-complete close check.
A ready proposal is insufficient. A model whose state says committed is also insufficient without that durable binding. Current-period close must use the independently frozen current ledger head and the other actual close kernels. The domain's aggregate integration checks these boundaries separately on a task-owned PostgreSQL instance with synthetic financial inputs.
Continue to Trial-Balance Validation to compare the complete booked account vector with an independently reported vector. Equal debit and credit totals alone cannot prove every account is right.
References3 primary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
R01 — Configured journal reversal and posting distinction
- Organization: Oracle; official Cloud Financials26d documentation.
- Title: Journal Reversals.
- URL: https://docs.oracle.com/en/cloud/saas/financials/26d/faugl/journal-reversals.html
- Accessed:25October2026; primary vendor operational documentation.
- Supports: reversal timing/method are configured; generating and posting reversal journals are distinct steps.
- Limitations: our UTC fixed-estimate policy, schemas, bounds and retry model are authored; no Oracle conformance claim or copied transaction table.
R02 — Accrual reversal operational examples
- Organization: Oracle; official Cloud Financials26b documentation.
- Title: Examples of Accrual Reversal.
- URL: https://docs.oracle.com/en/cloud/saas/financials/26b/faiac/examples-of-accrual-reversal.html
- Accessed:25October2026; primary vendor methodology/example context.
- Supports: a selected accrual/reversal workflow is one operational mechanism rather than a universal recognition decision.
- Limitations: Cedar amounts and events are independent synthetic input; no published example numbers are reused. Customer-interest entitlement, invoice accounting and framework recognition are excluded.
R03 — Authored, independently prescribed contract
- Frozen field contract: examples/DATA-CONTRACT.md.
- Before-core independent literal calendar/financial/chronology cases: datasets/independent-calendar.json.
- Full prescribed outputs: datasets/cases.json. Later regressions are separately identified in validation.
- Source classification: authored policy, synthetic teaching inputs and author-derived exact arithmetic; not third-party empirical evidence.
Historical-example decision
Not useful for this bounded tutorial without approved terms, complete source and journal chronology, applicable accounting policy and reusable rights. No actual corporate estimate or journal is claimed. Limitations include trusted host source/authentication, real atomic acceptance, periodic worker availability and the chosen UTC calendar.
Full dependency-light reference implementations in both supported languages.
/** UTC monthly estimates; pure planning and sequential modeled acceptance. */
type Json=any;
const SCOPE=['ledger_id','entity_id','schedule_id'];
const STATE=['schema_version',...SCOPE,'currency','records'];
const REQUEST=['schema_version',...SCOPE,'currency','period_start','period_end','template_head','template_versions'];
const HEAD=['schema_version','source_id',...SCOPE,'coverage_start','coverage_end','head_revision','observed_at','receipt_ref'];
const TEMPLATE=['schema_version','source_id','version_id','revision','supersedes_revision','effective_period','available_at','status','amount_minor','expense_account_id','payable_account_id'];
const COMMAND=['schema_version',...SCOPE,'currency','period','kind','expected_version','template_version_id','amount_minor','expense_account_id','payable_account_id','journal_id','business_operation_id','effective_at','booking_period','source_available_at'];
const ACCOUNT=['account_id','ledger_id','entity_id','currency','account_type','normal_side','status'];
const NORMAL: Record<string,string>={asset:'debit',expense:'debit',liability:'credit',income:'credit',equity:'credit'};
class Invalid extends Error {
constructor(code: string,readonly status='rejected'){super(code);}
}
function need(ok: boolean,code: string,status='rejected'): asserts ok {
if(!ok)throw new Invalid(code,status);
}
function exact(value: Json,keys: string[]): void {
need(value!==null&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===keys.length&&keys.every(k=>Object.hasOwn(value,k)),'INVALID_SCHEMA');
}
function full(regex: RegExp,value: Json): boolean {
if(typeof value!=='string')return false;
const match=regex.exec(value);return match!==null&&match[0].length===value.length;
}
function identifier(value: Json): void {need(full(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/,value),'INVALID_IDENTIFIER');}
function num(value: Json,positive=false): bigint {
need(full(positive?/^[1-9][0-9]{0,29}$/:/^(?:0|[1-9][0-9]{0,29})$/,value),'INVALID_AMOUNT');return BigInt(value);
}
function month(value: Json): number {
need(full(/^[0-9]{4}-(?:0[1-9]|1[0-2])$/,value)&&value.slice(0,4)!=='0000','INVALID_PERIOD');
return (Number(value.slice(0,4))-1)*12+Number(value.slice(5))-1;
}
function monthText(index: number): string {
need(index>=0&&index<9999*12,'INVALID_PERIOD');return String(Math.floor(index/12)+1).padStart(4,'0')+'-'+String(index%12+1).padStart(2,'0');
}
function timestamp(value: Json): void {
need(full(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/,value),'INVALID_TIMESTAMP');
const year=Number(value.slice(0,4)),m=Number(value.slice(5,7)),d=Number(value.slice(8,10));
const leap=year%4===0&&(year%100!==0||year%400===0),days=[31,leap?29:28,31,30,31,30,31,31,30,31,30,31];
need(year>=1&&m>=1&&m<=12&&d>=1&&d<=days[m-1]&&Number(value.slice(11,13))<24&&Number(value.slice(14,16))<60&&Number(value.slice(17,19))<60,'INVALID_TIMESTAMP');
}
export function dueTimes(period: string): string[] {
const following=monthText(month(period)+1),first=following+'-01T00:00:00.000Z';
return [new Date(Date.parse(first)-1).toISOString(),first];
}
function copy(value: Json): Json {return JSON.parse(JSON.stringify(value));}
function semantic(value: Json): string {
if(Array.isArray(value))return '['+value.map(semantic).join(',')+']';
if(value!==null&&typeof value==='object')return '{'+Object.keys(value).sort().map(k=>JSON.stringify(k)+':'+semantic(value[k])).join(',')+'}';
return JSON.stringify(value);
}
function equal(a: Json,b: Json): boolean {return semantic(a)===semantic(b);}
function key(command: Json): string {return command.period+'/'+command.kind;}
function authorize(context: Json,scope: Json): void {
exact(context,['authorized_scopes','now','accounts','open_periods']);
need(Array.isArray(context.authorized_scopes)&&context.authorized_scopes.length<=1000,'INVALID_CONTEXT');
const seen=new Set<string>();
for(const item of context.authorized_scopes){exact(item,SCOPE);for(const name of SCOPE)identifier(item[name]);const identity=JSON.stringify(SCOPE.map(k=>item[k]));need(!seen.has(identity),'INVALID_CONTEXT');seen.add(identity);}
need(seen.has(JSON.stringify(SCOPE.map(k=>scope[k]))),'SCOPE_FORBIDDEN');
}
function currentContext(context: Json): Map<string,Json> {
timestamp(context.now);const accounts=context.accounts,periods=context.open_periods;
need(Array.isArray(accounts)&&accounts.length<=32,'INVALID_ACCOUNTS');const lookup=new Map<string,Json>();
for(const account of accounts){
exact(account,ACCOUNT);for(const name of ['account_id','ledger_id','entity_id'])identifier(account[name]);
need(!lookup.has(account.account_id),'INVALID_ACCOUNTS');
need(typeof account.account_type==='string'&&Object.hasOwn(NORMAL,account.account_type),'INVALID_ACCOUNTS');
need(account.normal_side===NORMAL[account.account_type]&&['USD','EUR'].includes(account.currency)&&['active','closed'].includes(account.status),'INVALID_ACCOUNTS');lookup.set(account.account_id,account);
}
need(Array.isArray(periods)&&periods.length<=25,'INVALID_CONTEXT');for(const item of periods)month(item);need(new Set(periods).size===periods.length,'INVALID_CONTEXT');return lookup;
}
function commandShape(command: Json): void {
exact(command,COMMAND);need(command.schema_version==='d30-accrual-command-v1','UNSUPPORTED_SCHEMA');
for(const name of [...SCOPE,'template_version_id','expense_account_id','payable_account_id','journal_id','business_operation_id'])identifier(command[name]);
need(command.currency==='USD','UNSUPPORTED_CURRENCY');need(['accrual','reversal'].includes(command.kind),'INVALID_KIND');
num(command.expected_version);num(command.amount_minor,true);month(command.period);month(command.booking_period);timestamp(command.effective_at);timestamp(command.source_available_at);
need(command.expense_account_id!==command.payable_account_id,'ACCOUNT_ROLE_MISMATCH');const expected=dueTimes(command.period)[command.kind==='accrual'?0:1];
need(command.effective_at===expected&&command.booking_period===expected.slice(0,7),'INSTRUCTION_MISMATCH');
}
function resultFor(command: Json,recordedAt: string,version: number,original: Json): Json {
let legs=[{account_id:command.expense_account_id,side:'debit',amount_minor:command.amount_minor},{account_id:command.payable_account_id,side:'credit',amount_minor:command.amount_minor}];
if(command.kind==='reversal')legs=original.result.journal.legs.map((leg: Json)=>({...leg,side:leg.side==='debit'?'credit':'debit'}));
const journal: Json=Object.fromEntries(['ledger_id','entity_id','journal_id','business_operation_id','currency','effective_at','source_available_at','booking_period'].map(k=>[k,command[k]]));
Object.assign(journal,{schema_version:'d30-journal-v1',legs});
const instruction=Object.fromEntries([...SCOPE,'period','kind'].map(k=>[k,command[k]]));
return {instruction_key:instruction,version:String(version),recorded_at:recordedAt,journal,original_journal_id:original===null?null:original.command.journal_id};
}
type Accepted={accepted: Map<string,Json>,journalIds: Set<string>,businessIds: Set<string>,previous: string|null};
function validateState(state: Json): Accepted {
try {
exact(state,STATE);need(state.schema_version==='d30-accrual-state-v1','INVALID_STATE');for(const name of SCOPE)identifier(state[name]);
need(state.currency==='USD'&&Array.isArray(state.records)&&state.records.length<=48,'INVALID_STATE');
const accepted=new Map<string,Json>(),journalIds=new Set<string>(),businessIds=new Set<string>();let previous: string|null=null;
for(let index=0;index<state.records.length;index++){
const record=state.records[index];exact(record,['command','result']);const command=record.command,result=record.result;commandShape(command);
need(SCOPE.every(k=>command[k]===state[k]),'INVALID_STATE');need(result!==null&&typeof result==='object'&&!Array.isArray(result)&&Object.hasOwn(result,'recorded_at'),'INVALID_STATE');
const clock=result.recorded_at;timestamp(clock);need(previous===null||previous<=clock,'INVALID_STATE');
need(command.expected_version===String(index)&&command.effective_at<=clock&&command.source_available_at<=clock,'INVALID_STATE');
need(!accepted.has(key(command))&&!journalIds.has(command.journal_id)&&!businessIds.has(command.business_operation_id),'INVALID_STATE');
const original=command.kind==='reversal'?(accepted.get(command.period+'/accrual')??null):null;
if(command.kind==='reversal'){
need(original!==null,'INVALID_STATE');need(['template_version_id','amount_minor','expense_account_id','payable_account_id'].every(k=>command[k]===original.command[k]),'INVALID_STATE');
need(command.source_available_at>=original.result.recorded_at,'INVALID_STATE');
}
need(equal(result,resultFor(command,clock,index+1,original)),'INVALID_STATE');accepted.set(key(command),record);journalIds.add(command.journal_id);businessIds.add(command.business_operation_id);previous=clock;
}
return {accepted,journalIds,businessIds,previous};
} catch(error){if(error instanceof Invalid||error instanceof TypeError)throw new Invalid('INVALID_STATE');throw error;}
}
function templates(request: Json,state: Json,now: string): {selected: Map<string,Json>,coverage: Json} {
exact(request,REQUEST);need(request.schema_version==='d30-accrual-plan-v1','UNSUPPORTED_SCHEMA');for(const name of SCOPE)identifier(request[name]);
need(SCOPE.every(k=>request[k]===state[k]),'SCOPE_MISMATCH');need(request.currency==='USD','UNSUPPORTED_CURRENCY');
const start=month(request.period_start),end=month(request.period_end);need(end-start>=1&&end-start<=24,'INVALID_PERIOD');
const head=request.template_head;exact(head,HEAD);need(head.schema_version==='d30-accrual-template-head-v1','INVALID_TEMPLATE_HEAD');
for(const name of [...SCOPE,'source_id','receipt_ref'])identifier(head[name]);need(SCOPE.every(k=>head[k]===request[k]),'SCOPE_MISMATCH');
const first=month(head.coverage_start),last=month(head.coverage_end);need(first<=start&&end<=last,'TEMPLATE_COVERAGE_GAP','indeterminate');
timestamp(head.observed_at);const countBig=num(head.head_revision);need(countBig<=32n,'INVALID_TEMPLATE_HEAD');const count=Number(countBig);
const versions=request.template_versions;need(Array.isArray(versions)&&versions.length<=32,'INVALID_TEMPLATE_HISTORY');
const ids=new Map<string,Json>(),revisions=new Map<bigint,Json>(),parsed: {revision: bigint,effective: number,version: Json}[]=[];
for(const version of versions){
exact(version,TEMPLATE);need(version.schema_version==='d30-accrual-template-v1','INVALID_TEMPLATE');for(const name of ['source_id','version_id'])identifier(version[name]);
need(version.source_id===head.source_id,'SOURCE_MISMATCH');const revision=num(version.revision,true),effective=month(version.effective_period);timestamp(version.available_at);need(['active','cancelled'].includes(version.status),'INVALID_TEMPLATE');
if(version.status==='active'){
num(version.amount_minor,true);identifier(version.expense_account_id);identifier(version.payable_account_id);need(version.expense_account_id!==version.payable_account_id,'ACCOUNT_ROLE_MISMATCH');
}else need(['amount_minor','expense_account_id','payable_account_id'].every(k=>version[k]===null),'INVALID_TEMPLATE');
if(ids.has(version.version_id))need(equal(version,ids.get(version.version_id)),'TEMPLATE_ID_CONFLICT');ids.set(version.version_id,version);
if(revisions.has(revision))need(equal(version,revisions.get(revision)),'TEMPLATE_REVISION_CONFLICT');revisions.set(revision,version);parsed.push({revision,effective,version});
}
need(ids.size===versions.length&&revisions.size===versions.length,'DUPLICATE_TEMPLATE');need([...revisions.keys()].every(rev=>rev<=countBig),'TEMPLATE_HEAD_MISMATCH');
need(revisions.size===count&&Array.from({length:count},(_,i)=>revisions.has(BigInt(i+1))).every(Boolean),'INCOMPLETE_TEMPLATE_HISTORY','indeterminate');
parsed.sort((a,b)=>a.revision<b.revision?-1:1);let previousClock: string|null=null,previousPeriod: number|null=null;
for(const {revision,effective,version} of parsed){
need(version.supersedes_revision===(revision===1n?null:String(revision-1n)),'INVALID_TEMPLATE_CHAIN');need(previousPeriod===null||effective>previousPeriod,'INVALID_TEMPLATE_CHAIN');
const clock=version.available_at;need(clock<=head.observed_at&&(previousClock===null||previousClock<=clock),'TEMPLATE_CLOCK_VIOLATION');
need(clock<=version.effective_period+'-01T00:00:00.000Z','RETROACTIVE_SCHEDULE_POLICY_UNSUPPORTED');previousClock=clock;previousPeriod=effective;
}
for(const record of state.records){
const command=record.command;if(command.kind!=='accrual')continue;const known=ids.get(command.template_version_id);
need(known!==undefined,'MISSING_ACCEPTED_TEMPLATE','indeterminate');
need(known.status==='active'&&['amount_minor','expense_account_id','payable_account_id'].every(k=>known[k]===command[k])
&&known.effective_period<=command.period&&known.available_at<=record.result.recorded_at,'TEMPLATE_ID_CONFLICT');
}
need(now<=head.observed_at,'KNOWLEDGE_BEYOND_TEMPLATE_SOURCE','indeterminate');const visible=parsed.filter(p=>p.version.available_at<=now);
const visibleRevision=visible.length?String(visible[visible.length-1].revision):'0';
const coverage: Json=Object.fromEntries(['source_id','receipt_ref','observed_at','head_revision'].map(k=>[k,head[k]]));Object.assign(coverage,{validated_through_revision:String(count),visible_through_revision:visibleRevision});
const selected=new Map<string,Json>();for(let index=start;index<end;index++){
const applicable=visible.filter(p=>p.effective<=index);need(applicable.length>0,'MISSING_TEMPLATE','indeterminate');selected.set(monthText(index),applicable[applicable.length-1].version);
}
return {selected,coverage};
}
function accountRoles(lookup: Map<string,Json>,scope: Json,expense: string,payable: string): void {
for(const [aid,kind,normal] of [[expense,'expense','debit'],[payable,'liability','credit']]){
const account=lookup.get(aid);need(account!==undefined&&['ledger_id','entity_id'].every(k=>account[k]===scope[k])&&account.currency==='USD'&&account.account_type===kind&&account.normal_side===normal&&account.status==='active','ACCOUNT_ROLE_MISMATCH');
}
}
function instruction(period: string,kind: string,version: Json,now: string,accepted: Map<string,Json>,openPeriods: string[]): Json {
const original=accepted.get(period+'/accrual')??null,basis=original!==null&&kind==='reversal'?original.command:version;
const due=dueTimes(period)[kind==='accrual'?0:1];let code='READY';
if(kind==='reversal'&&original===null)code='ORIGINAL_NOT_ACCEPTED';else if(!openPeriods.includes(due.slice(0,7)))code='BOOKING_PERIOD_CLOSED';
return {period,kind,due_at:due,template_version_id:basis.template_version_id??basis.version_id,amount_minor:basis.amount_minor,
expense_account_id:basis.expense_account_id,payable_account_id:basis.payable_account_id,booking_period:due.slice(0,7),source_available_at:now,status:code==='READY'?'ready':'blocked',code,
original_journal_id:original===null||kind==='accrual'?null:original.command.journal_id};
}
export function planDueAccruals(state: Json,request: Json,context: Json): Json {
try{
exact(request,REQUEST);for(const name of SCOPE)identifier(request[name]);authorize(context,request);
const {accepted,previous}=validateState(state),lookup=currentContext(context),now=context.now;need(previous===null||previous<=now,'RECORDED_TIME_REGRESSION');
const {selected,coverage}=templates(request,state,now),pending: Json[]=[];
for(const [period,version] of selected){
const original=accepted.get(period+'/accrual')??null;if(version.status==='cancelled'&&original===null)continue;
for(const kind of ['accrual','reversal']){
if(accepted.has(period+'/'+kind))continue;const due=dueTimes(period)[kind==='accrual'?0:1];if(due>now)continue;
const item=instruction(period,kind,version,now,accepted,context.open_periods);accountRoles(lookup,state,item.expense_account_id,item.payable_account_id);pending.push(item);
}
}
pending.sort((a,b)=>a.due_at<b.due_at?-1:a.due_at>b.due_at?1:a.kind<b.kind?-1:a.kind>b.kind?1:0);
return copy({status:'ok',code:'SCHEDULE_PLAN_COMPLETE',coverage,instructions:pending,all_due_accepted:pending.length===0});
}catch(error){if(error instanceof Invalid)return {status:error.status,code:error.message,coverage:null,instructions:[],all_due_accepted:null};throw error;}
}
export function processScheduledAccrual(state: Json,command: Json,request: Json,context: Json): Json {
try{
commandShape(command);authorize(context,command);const {accepted,journalIds,businessIds,previous}=validateState(state);need(SCOPE.every(k=>command[k]===state[k]),'SCOPE_MISMATCH');
const prior=accepted.get(key(command));if(prior!==undefined){need(equal(prior.command,command),'INSTRUCTION_CONFLICT');return copy({status:'replayed',code:'SCHEDULED_POSTING_REPLAYED',result:prior.result,state});}
const lookup=currentContext(context),now=context.now;need(previous===null||previous<=now,'RECORDED_TIME_REGRESSION');const {selected}=templates(request,state,now);
need(selected.has(command.period),'PERIOD_OUTSIDE_PLAN');need(command.expected_version===String(state.records.length),'VERSION_CONFLICT');need(state.records.length<48,'STATE_CAPACITY');
need(!journalIds.has(command.journal_id)&&!businessIds.has(command.business_operation_id),'JOURNAL_IDENTITY_CONFLICT');const version=selected.get(command.period),original=accepted.get(command.period+'/accrual')??null;
if(command.kind==='accrual')need(version.status==='active','SCHEDULE_CANCELLED');else need(original!==null,'ORIGINAL_NOT_ACCEPTED');
const expected=instruction(command.period,command.kind,version,now,accepted,context.open_periods);need(command.effective_at<=now,'INSTRUCTION_NOT_DUE');
need(['template_version_id','amount_minor','expense_account_id','payable_account_id','booking_period'].every(k=>command[k]===expected[k]),'INSTRUCTION_MISMATCH');
need(command.source_available_at>=(command.kind==='accrual'?version.available_at:original.result.recorded_at)&&command.source_available_at<=now,'SOURCE_CLOCK_VIOLATION');
need(context.open_periods.includes(command.booking_period),'BOOKING_PERIOD_CLOSED');accountRoles(lookup,state,command.expense_account_id,command.payable_account_id);
const result=resultFor(command,now,state.records.length+1,command.kind==='reversal'?original:null),updated=copy(state);updated.records.push({command:copy(command),result:copy(result)});
return copy({status:'committed',code:'SCHEDULED_POSTING_RECORDED',result,state:updated});
}catch(error){if(error instanceof Invalid)return {status:error.status,code:error.message,result:null,state:copy(state)};throw error;}
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.
