Allocations — Technical Implementation
How Jules implements the allocation matching system — data model, creation flow, fulfilment state machines, document generation, and ERP sync.
Allocations — Technical Implementation
Developer documentation — Deep dive into the allocation system: data model, creation/batch flows, fulfilment step state machines, stepper configuration, document generation, and integration hooks.
Table of Contents
- Overview
- Data Model
- Creation Flow
- Batch Creation
- Blacklist Validation
- Fulfilment Step State Machines
- Stepper Configuration
- Document Generation
- ERP Synchronization
- Warehouse Allocations
- Searates Tracking
- GraphQL API Surface
Overview
An allocation is the join entity between a buy-side OperationQuality and a sell-side OperationQuality at the container level. It is the primary entity driving:
- Commercial margin calculation
- Logistics fulfilment tracking
- Document generation (Annex 7, load report, custom invoice)
- Tracking and delivery confirmation
Data Model
Allocation
model Allocation {
id String @id @default(uuid())
haroldNumber String @unique
status AllocationStatus // DRAFT | CONFIRMED
// Commercial links
buyOperationQualityId String
buyOperationQuality OperationQuality @relation("buy", fields: [buyOperationQualityId], references: [id])
sellOperationQualityId String?
sellOperationQuality OperationQuality? @relation("sell", fields: [sellOperationQualityId], references: [id])
// Optional warehouse link (instead of sell operation)
stockpileId String?
warehouseId String?
// References
referenceNumber String?
comment String?
// Tracking
trackingStatus AllocationTrackingStatus?
// Fulfilment steps (stored as JSON or related records)
annex7Status Annex7Status
bookingFulfilmentStatus BookingFulfilmentStatus
customsStatus CustomsStatus
loadReportStatus LoadReportStatus
isVGMSubmitted Boolean @default(false)
loadDetailsCount Int @default(0)
// Financial
marginPerTon Decimal?
marginCurrency String?
// ERP
erpId String?
erpValue String?
// Metadata
createdById String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
containers ContainerToAllocation[]
watchers AllocationWatcher[]
photos AllocationPhoto[]
documents GeneratedDocument[]
}
AllocationTrackingStatus enum
LOADED | GATED_IN | SHIPPED | ARRIVED | GATED_OUT | RETURNED | RECOVERY_NOTE_SENT | RECOVERED
Fulfilment step enums
enum Annex7Status {
PENDING
PREPARED_IN_ERP
SENT_TO_COMPLIANCE
SENT_TO_SUPPLIER
SIGNED_AND_UPLOADED
}
enum BookingFulfilmentStatus {
PENDING
PREPARED_IN_ERP
PRECARRIAGE_BOOKING_OK
FREIGHT_BOOKING_OK
ALL_BOOKING_OK
}
enum CustomsStatus {
PENDING
SENT_TO_AGENT
SENT_TO_CARRIER
}
enum LoadReportStatus {
PENDING
PREPARED
SENT_TO_DOCS_TEAM
}
Creation Flow
Single allocation creation
Input validation:
buyOperationQualityIdmust reference a confirmed buy operation qualitysellOperationQualityIdmust reference a confirmed sell operation quality (or be null for warehouse allocations)- Container IDs must belong to the buy operation
- No container can be allocated twice to the same sell operation quality
Initialization:
haroldNumbergenerated (sequence:ALLOC-{YEAR}-{SEQ})- All fulfilment step statuses initialized to
PENDING isVGMSubmitted= falsestatus=DRAFT(orCONFIRMEDif passed in input)
Side effects on confirmation:
- Container
followUpStatusmay transition - Margin calculation triggered
- Watchers notified
Batch Creation
createManyAllocations creates one allocation per container in a single operation:
// Input
{
buyOperationQualityId: string
sellOperationQualityId: string
containerIds: string[] // one allocation per container ID
status: AllocationStatus
referenceNumber?: string
comment?: string
}
// Output: Allocation[] (one per containerIds entry)
Implementation:
- Validate the buy/sell quality pair once
- Run blacklist check once (same buy site → same check applies to all)
- For each
containerId, runcreateAllocationwith the same parameters - Return all created allocations
Atomicity: the entire batch runs in a single Prisma transaction — either all allocations are created or none are.
Blacklist Validation
Before an allocation can be confirmed (status = CONFIRMED), Jules checks the site blacklist:
The check queries SiteToBlacklistedSite where:
siteId= the sell operation's customer site IDblacklistedSiteId= the buy operation's supplier site ID
If a match is found, the mutation throws a ValidationError with a descriptive message.
Fulfilment Step State Machines
Each of the 6 fulfilment steps has its own state machine. They are independent — advancing one step does not automatically advance others.
Annex 7 state machine
Booking fulfilment state machine
Updating step status
Each step is updated via a dedicated mutation field:
| Step | Mutation | Input |
|---|---|---|
| Annex 7 | updateAllocationAnnex7Status | { id, annex7Status } |
| Booking | updateAllocationBookingStatus | { id, bookingFulfilmentStatus } |
| Customs | updateAllocationCustomsStatus | { id, customsStatus } |
| Load Report | updateAllocationLoadReportStatus | { id, loadReportStatus } |
| VGM | updateAllocationVGM | { id, isVGMSubmitted } |
| Load Details | updateAllocationLoadDetails | { id, loadDetailsCount } |
Or via updateAllocation with the step field included in the update input.
Stepper Configuration
Organizations can customize which fulfilment steps are visible and in what order via AllocationStepperConfig:
// stepperConfigs: which steps are enabled
type AllocationStepperConfig = {
annex7: boolean
booking: boolean
customs: boolean
loadReport: boolean
vgm: boolean
loadDetails: boolean
order: string[] // e.g. ['annex7', 'booking', 'customs', 'loadReport', 'vgm']
}
// stepperValues: current values of each step (the status fields above)
type AllocationStepperValues = {
annex7Status: Annex7Status
bookingFulfilmentStatus: BookingFulfilmentStatus
customsStatus: CustomsStatus
loadReportStatus: LoadReportStatus
isVGMSubmitted: boolean
loadDetailsCount: number
}
The stepper config is stored per organization in the settings and applied at query time. The frontend reads stepperConfigs to determine which steps to render.
Document Generation
Allocations are the primary source for document generation:
| Document | Mutation | Output |
|---|---|---|
| Annex 7 | generateAnnex7 | PDF — environmental compliance form |
| Custom Invoice | generateCustomInvoice | PDF — commercial invoice for customs |
| Load Report | generateLoadReport | PDF — container weights and contents |
| Recovery Note | generateRecoveryNote | PDF — confirmation of material recovery |
Generation pipeline
GeneratedDocument model
model GeneratedDocument {
id String @id
allocationId String
type DocumentType // ANNEX7 | CUSTOM_INVOICE | LOAD_REPORT | RECOVERY_NOTE
fileUrl String
generatedAt DateTime
generatedById String
}
Photos
model AllocationPhoto {
id String @id
allocationId String
type PhotoType // ANNEX7 | GENERAL
fileUrl String
uploadedAt DateTime
}
ERP Synchronization
synchronizeAllocationToErp pushes the allocation to the external ERP:
- Load allocation with buy/sell operation data and container list
- Validate: allocation must be
CONFIRMED - Build ERP payload (organization-specific mapping)
- POST to ERP integration endpoint
- Store ERP response:
erpId,erpValueon the allocation
ERP sync status can be checked by the presence/absence of erpId.
Warehouse Allocations
When an allocation targets a warehouse (instead of a sell operation):
// Create warehouse allocation
createAllocation({
buyOperationQualityId: "...",
sellOperationQualityId: null, // no sell operation
stockpileId: "...", // target stockpile
warehouseId: "...", // target warehouse site
containerIds: [...]
})
Key differences from standard allocations:
- No sell operation quality linked
marginPerTonis not available at creation (margin is calculated when material is sold from stockpile)trackingStatusstill applies (containers are physically moved to the warehouse)- Document generation (load report) still works
Searates Tracking
Each allocation can have a Searates tracking record:
type SearatesTracking = {
allocationId: string
latitude: number
longitude: number
locationName: string
countryCode: string
events: SearatesEvent[]
vessel: string
voyage: string
updatedAt: DateTime
}
type SearatesEvent = {
eventType: string
location: string
eventDate: DateTime
description: string
}
Tracking data is fetched via the Searates API using the container reference number and updated on a scheduled basis or on demand.
GraphQL API Surface
Queries
| Query | Description |
|---|---|
filteredAllocations | List allocations with filters (status, tracking, ops, date ranges) |
allocation(id) | Single allocation by ID |
allocationByHaroldNumber(haroldNumber) | Lookup by Harold number |
Mutations
| Mutation | Description |
|---|---|
createAllocation | Single allocation |
createManyAllocations | Batch creation (one per container) |
updateAllocation | Update any fields |
updateManyAllocations | Batch update by IDs |
updateAllocationsBLByIds | Set BL number on multiple allocations |
updateAllocationsBLByReferenceNumbers | Set BL by reference numbers |
deleteAllocation | Delete a DRAFT allocation |
generateAnnex7 | Generate Annex 7 PDF |
generateCustomInvoice | Generate custom invoice PDF |
generateLoadReport | Generate load report PDF |
generateRecoveryNote | Generate recovery note PDF |
synchronizeAllocationToErp | Sync to external ERP |
addAllocationWatcher | Add a watcher |
removeAllocationWatcher | Remove a watcher |
Last updated today
Built with Documentation.AI