Coastal Shipping 14 — Storage, Demurrage & Detention Manager (coast_14_demurrage)
Work out, from confirmed container movements and a configurable policy, what a customer owes for the time a container sat in the yard, stayed beyond free time on the quay or stayed out with the customer — and publish that charge to billing exactly once. Configurable charge policies (start and end events, free time, calendar-day clock in an explicit timezone, tiered daily bands), exposures, approved suspensions and concessions, immutable charge assessments and the signed pre-tax entitlements.
Its central question:
Between which two confirmed movements was this container exposed, how many days of that are chargeable under the policy that applied when it started, and what is the signed pre-tax amount billing must raise — once?
coast_14_demurrage is application SHP-14 of the Coastal Shipping series (coast_NN_<name>, numbered by the pack catalogue; each able to run stand-alone or as a network member). It shares no tables and no foreign keys with its siblings (coast_03_tracking and coast_04_yard hold the movement and gate facts, coast_05_rating taxes and invoices, the finance interface posts) — they link by canonical_ref strings and by messages. All data is fictional; no tariff or tax claim is made.
The spine
StoragePolicy → StorageBand, StorageExposure → StorageSuspension / StorageConcession → StorageAssessment → StorageAssessmentBand → StorageEntitlement → StorageBillingAck, with StorageDispute, StorageException, StorageSourceSnapshot (the evidence) and StorageAudit around it.
The documents
| Page | What's in it |
|---|---|
| 00 — Overview | What the app is, who owns what, the rules, the 13 models, the demo scenario |
| 01 — Quick Reference | Menu map, every model, status vocabularies, the messages, how to run, seed and regenerate |
| 02 — System Diagram | The data model as diagrams (+ interactive viewer) and how the app links to its siblings |
Status
Built (handlers and workflow, tested): policy draft → active (independent supervisor, bands valid, frozen once active, calendar-day clocks only) → retired; exposures that start and end only on confirmed evidence of the right container, event and time, pin the policy they started under and never overlap for the same container and charge type (cross-charge overlap only when both policies say so); reconcile only with a physical time stated by the source, no conflicting or rejected evidence and no open clock dispute; approved suspensions (local date ranges counted once) and concessions (independent approval); the assessment as the pack's calendar-day calculation, stored as immutable clock and rate snapshots with a hash (provisional readings of open exposures are never approved); approval creates the entitlement once; submit publishes it once by canonical id and hash; billing's durable acknowledgement is the only way to ACCEPTED; a correction supersedes the approved assessment, publishing the exact reversal of the original entitlement and the replacement together; disputes; append-only evidence, acknowledgements and audit.
Control exceptions: the application raises INCONSISTENT_EVIDENCE (a changed copy of used evidence), EVIDENCE_REJECTED, DISPUTED_RATE (a dispute), BILLING_CONFLICT, BILLING_REJECTED and BILLING_MISMATCH itself; MISSING_EVIDENCE, OVERLAPPING_EXPOSURE and OTHER exist for people to record (a missing or overlapping event is refused where it happens, so no row is left behind).
Network handlers (tested): gate and movement evidence stored once (a changed copy is a CONFLICT, a rejected movement marks the evidence REJECTED and raises a clock-dispute exception), exposures that end when their end evidence arrives, billing acknowledgement (accepted, rejected, conflicting hash quarantined, replay), the supplemental invoice detail (the reversal must be the exact negation of the original's stored tax and total), assurance actions.
Not built: business-day and rolling-hour clocks (they need a configured calendar and a tested adapter; a policy that asks for one cannot be activated — there is no fallback); interim or recurring billing (one approved assessment per exposure at a time; later ones reverse and replace); commands (the pack gives this application none, so exposures are opened by people and ended by people or by evidence); the check that the same storage charge is not also produced by another tariff path in coast_05_rating (it cannot be seen from here).
Translation decisions (pack → native DSL)
As coast_10_release (fleet-unique Storage* names, canonical_ref on every model, cid on network entities, strings for cross-app references, Decimal(p,s), upper-case enums, name on FK targets, the pack's outbox/inbox models dropped). Specific to this app:
| Pack | Native | Why |
|---|---|---|
| S861 ChargePolicy … S871 Audit, S874 ExternalSnapshot | StoragePolicy, StorageBand, StorageExposure, StorageSuspension, StorageAssessment, StorageAssessmentBand, StorageConcession, StorageEntitlement, StorageDispute, StorageBillingAck, StorageAudit, StorageSourceSnapshot | model names are unique across the fleet |
| S872 OutboxEvent, S873 InboxMessage | dropped | the integration runtime provides them |
ClockSuspension from_at / to_at DateTime |
from_date / to_date Date |
the pack's README: suspensions are approved local date intervals |
ChargePolicy valid_from / valid_to Date |
Date | kept (a local calendar date) |
version |
version_no |
|
Exposure container_ref (required) |
container_canonical_ref (required) + container_cid / container_ref / container_number |
the series' reference pairing |
| (not in the pack) | StorageException |
the pack's generic exception_detected is storage.exception_raised / _resolved, as in Stage 2 |
| (not in the pack) | charge_code, entitlement_kind (CHARGE / REVERSAL / REPLACEMENT), exposure_id on the entitlement |
the pack's event carries a charge code; a reversal and a replacement must be told apart; the event payload reads its exposure |
| (not in the pack) | eligible_days, suspended_days, free_days_applied, concession_amount, provisional, calculated_by, approved_by, correction_reason, correction_approved_by on the assessment |
the guards need them; the calculation is shown, not hidden in JSON |
| (not in the pack) | evidence columns on the snapshot (container_number, event_code, occurred_at, time_basis) |
a clock may only start and stop on confirmed, timed evidence |
shipping.storage.entitlement_approved.v1 |
storage.entitlement_approved, published on SUBMIT (an approved entitlement is created at approval, released to billing on submit) |
the pack's workflow publishes in submit |
shipping.billing.entitlement_acknowledged.v1, …supplemental_detail_issued.v1, …document_issued.v1 |
rating.entitlement_acknowledged, rating.supplemental_detail_issued, rating.billing_document_issued |
<domain>.<name>; confirmed against coast_05_rating's Stage 3 integration DSL |
shipping.container.movement_accepted.v1 / _rejected.v1, shipping.yard.gate_recorded.v1 |
tracking.movement_accepted / tracking.movement_rejected, yard.gate_recorded |
<domain>.<name> |
Open points (for the network)
- The physical time of a movement.
tracking.movement_acceptedcarries noeffective_atin the pack (and not yet in coast_03_tracking's payload). This app readseffective_at(oroccurred_at) from the payload when it is there and marks the evidencetime_basis = PAYLOAD; without it the evidence is stored asENVELOPE(only when the message was sent) and cannot reconcile an exposure (a final entitlement needs the physical time).yard.gate_recordedcarriesgate_occurred_at, so gate evidence is timed. Until coast_03_tracking sendseffective_at, LOAD / DISCHARGE clocks must rest on gate evidence or a hand-fedeffective_at. - Exposures are opened by people. The pack gives this application no commands and its events carry no customer, so nothing can open an exposure from a movement alone. An open exposure ends by itself when its end evidence arrives.
- One tariff path. The same storage charge must not also be raised from the manifest rating in coast_05_rating; that is a billing configuration, not something this application can check.
- Billing document numbers in the seed (INV-26-0101, CN-26-0101) are placeholders: coast_05_rating's seed is not changed by this application.
- Simulated outcomes: the pack's worked example (EXP-26-0001) rests on simulated movement evidence (
MV-FX-0001/MV-FX-0002, payloadsimulated: true) because its dates are the scenario's fixture values, not the Stage 1 tracking story.
Prototype system; draft. All names, rates and amounts in the demo data are fictional. No real tariff, tax or legal claim is made.