User guide
Fulfillment
Looking for what it does rather than how to use it? Read the Fulfillment overview .
What it is
Fulfillment is how an order stops being a request and becomes something the client has received and can be billed for. Solidlio carries two order streams: client store orders, which an MSP approves and then walks through Processing → Shipped → Delivered by hand, and service orders, the single record for every service sale, which moves Draft → Approved → Completed.
Concepts
| Noun | What it is | ID prefix |
|---|---|---|
| Client store order | A basket a client placed in the MSP’s store. Holds line items, totals, currency, FX snapshot and shipping address | mso_ |
| Order line item | One product or custom item on a store order | moi_ |
| Service order | The single record for any service sale — direct, marketplace, support block, project, managed service, escalation | sord_ |
| Platform fee | Solidlio’s cut, written as its own row when a service order is submitted | — |
| Invoice | Produced from a Delivered net-terms store order by the nightly invoicing job | inv_ |
The two lifecycles
Client store order (MspStoreOrderStatus):
PENDING_APPROVAL ──approve──> APPROVED ──> PROCESSING ──> SHIPPED ──> DELIVERED
│
└──reject──> CANCELLED
Forward skips are legal (APPROVED straight to DELIVERED); backward moves are not.
- Approve and reject are only legal from
PENDING_APPROVAL. - Fulfillment is forward-only. From
APPROVEDyou may jump toPROCESSING,SHIPPEDorDELIVERED; fromPROCESSINGtoSHIPPEDorDELIVERED; fromSHIPPEDonly toDELIVERED. DELIVEREDandCANCELLEDare terminal — there is no backward edge and no post-approval cancel.
Service order (ServiceOrderStatus):
DRAFT ──submit──> APPROVED ──complete──> COMPLETED
submitauto-approves; there is no separate approver step.completeis legal fromAPPROVEDorIN_PROGRESS, and setsfulfillmentStatus = COMPLETEDandcompletedAtat the same time.- Editing is only permitted while the order is
DRAFTorPENDING_APPROVAL.
Order types and billing models (service orders)
Order type and billing model are independent fields — the type says what was sold, the billing model says how it is priced. Any type may carry any model.
orderType—DIRECT_SALE,MARKETPLACE_SALE,SUPPORT_BLOCK,PROJECT,MANAGED_SERVICE,CONSULTING,TRAINING,ESCALATIONbillingModel—FLAT_RATE,PER_USER,PER_DEVICE,BLOCK_HOURS,HOURLY,PER_INCIDENT,PROJECT,HYBRID,USAGEpaymentTerms— defaults toNET_30
Where service orders come from
Three writers create a ServiceOrder:
| Writer | Status it lands in | Order number |
|---|---|---|
| Scheduled billing run (contracts & recurring) | APPROVED | assigned by the run |
| Escalation billing finalisation | COMPLETED | none |
Roles and permissions
Store order actions run behind a path-scoped guard on /store writes.
| Action | platform administrator | MSP administrator | MSP technician | organization administrator | power user | CUSTOMER |
|---|---|---|---|---|---|---|
| List store orders (whole account) | ✓ | ✓ | ✓ | — | — | — |
| List store orders (own org only) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Read a sibling client’s store order | — | — | — | — | — | — |
| Approve / reject a store order | ✓ | ✓ | — | — | — | — |
| Advance fulfillment | ✓ | ✓ | — | — | — | — |
| AI triage a store order | ✓ | ✓ | — | — | — | — |
| Read service orders | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create / edit / submit / complete a service order | ✓ | ✓ | — | — | — | — |
Notes:
- “Store staff” is anyone at MSP technician rank or above. Staff may filter the store order list by any organization in the account. Everyone else is pinned to their own organization — a client-supplied
?organizationId=is ignored, and a sibling client’s order returns 404, not 403. - A non-staff caller with no organization context is rejected with
Organization context required. - Service order reads are scoped to orders where the caller’s account is the seller or the buyer. There is no cross-account read.
Walkthrough 1 — fulfilling a client store order
MSP portal → Store → Client Orders (/msp/store/orders)
- Review what came in. The page opens on a filter row: All, Pending Approval, Processing, Shipped, Delivered. Header tiles summarise the orders currently loaded — awaiting approval, in flight, and their value.
- Approve or reject. A
PENDING_APPROVALrow carries Approve and Reject buttons. Reject asks for a reason, sets the order toCANCELLEDand writesRejected: <reason>into the order notes. Both are final — an order cannot be re-opened after either. - Optional: AI triage. Run triage on an order to get a stored assessment that renders inline on the row.
- Start processing. On an
APPROVEDorder the action button reads Start processing. Click it and the order moves toPROCESSING. - Ship it. On a
PROCESSINGorder the button reads Mark shipped. You are prompted for a carrier and a tracking number — both optional, both capped at 120 characters. The order’sshippedAtis stamped the first time it reachesSHIPPED(orDELIVERED, if you skip ahead). - Deliver it. On a
SHIPPEDorder the button reads Mark delivered.deliveredAtis stamped once. This is the step that makes a net-terms order invoiceable. - The client sees it. In the org portal under Store → Orders (
/org/orders) and in the end-user portal (/user/orders), the client’s order detail renders a five-step timeline — Placed, Approved, Processing, Shipped, Delivered — plus the carrier, tracking number, ship date and delivery date as soon as you enter them.
Approval records approvedBy and approvedAt; every fulfillment call records the acting user in fulfilledBy. Rejection records the reason in the order notes but does not record who rejected it.
Walkthrough 2 — raising and closing a service order
MSP portal → Billing → Billing Overview → Service Orders (/msp/billing/orders)
- New order (
/msp/billing/orders/new). Pick an order type and billing model, name the service, set unit price, quantity, discount and tax, and choose payment terms (defaultNET_30). - Pricing is computed server-side. Subtotal is
unitPrice × quantity; total issubtotal − discount + tax, floored at zero; the platform fee is 12% of the total, also floored at zero. A discount larger than the subtotal cannot produce a negative total or a negative fee. - Save as draft, or submit in one step. Saving leaves the order in
DRAFT. PassingsubmitForProcessingcreates the order alreadyAPPROVEDwith its platform-fee row written, inside a single transaction — so a failure can never leave an approved order with no fee, or a fee with no order. - Submit a draft (
/msp/billing/orders/[id], Submit Order). The order becomesAPPROVEDand aPENDINGplatform-fee row is written against your account. - Mark complete. From
APPROVEDorIN_PROGRESS, Mark Complete sets the status toCOMPLETED, the fulfillment status toCOMPLETED, and stampscompletedAt. - See the money. Billing → Revenue (
/msp/billing/revenue) breaks revenue down by month, by order type, by service and by client, and lists every platform fee with a running total.
Walkthrough 3 — Delivered orders become invoices
This runs unattended.
dryRunis propagated downstream and the echo is verified: a summary that comes back withoutdryRun: trueis reported as a failure, not as a clean dry run, because it means real clients were invoiced.- A Canadian order whose tax jurisdiction cannot be resolved is put on tax hold rather than invoiced at $0.
Configuration
| Setting | Where | Effect if unset |
|---|---|---|
paymentMethod on the order | Set at checkout | Defaults to NET30. Only net-terms orders are picked up by the invoicing job |
currency + fxRates | Snapshotted at checkout | Defaults to CAD. The snapshot means the order re-prices identically forever, even after rates move |
paymentTerms (service order) | Order form | Defaults to NET_30 |
There is no configurable carrier list, no rate-shopping, and no label purchase. Carrier is a free-text field.
Plan tiers
Neither store order fulfillment nor service orders carry a plan gate. Every tier — MSP Free, Starter, Growth, Scale and Enterprise — has the full capability. Access is decided by role, not by plan.
Troubleshooting
| What you see | What it means |
|---|---|
Cannot move a PENDING_APPROVAL order to SHIPPED. Approve the order first, or it is already closed. | Fulfillment only starts after approval. Approve the order, then advance it. |
Cannot move a DELIVERED order to PROCESSING. … | DELIVERED is terminal. There is no way back. Raise a correction through billing, not through the order. |
Order cannot be rejected in current status | Reject is only legal from PENDING_APPROVAL. An approved order has no cancel path. |
Organization context required (403) | A non-staff caller reached the store order list with no organization on the token. Re-authenticate in a portal that pins an organization. |
Order not found (404) on an order you can see the number for | You are a buyer and the order belongs to a sibling client organization. This is deliberate — sibling orders are indistinguishable from non-existent ones. |
Order must be in DRAFT status to submit | The service order was already submitted. Its platform fee already exists; submitting again would double-charge. |
Order must be APPROVED or IN_PROGRESS to complete | Submit the draft first. |
Cannot update order in current status | A service order is only editable while DRAFT or PENDING_APPROVAL. |
clientOrganizationId does not belong to this account | The organization you attributed the order to is not one of yours. Use clientName / clientEmail for an off-platform client. |
buyerAccountId is only valid with a marketplaceSubscriptionId you own | A buyer account can only be derived from a marketplace subscription against a listing your account provides. |
marketplaceSubscriptionId not found or not owned by this account | Same rule, other direction — the subscription is not against one of your listings. |
orderType, serviceName, billingModel, and unitPriceCents are required | The service order form was submitted with a missing required field. |
Limits and known behaviour
Read this section before promising anything to a client.
- Fulfillment is manual and forward-only. Solidlio does not integrate with any carrier. Nothing polls a tracking number, nothing validates it, and nothing imports carrier scan events. Carrier and tracking number are free text the MSP types in, and status only moves when a person clicks.
- There is no post-approval cancel. Once an order is
APPROVEDthe only outward path is forward toDELIVERED. Reject is available only while the order is stillPENDING_APPROVAL. DELIVEREDis irreversible, and it is the trigger for invoicing. Marking an order delivered by mistake will cause a net-terms order to be invoiced on the next job run.- Fulfillment sends no notification. Advancing an order to
SHIPPEDorDELIVEREDwrites the database and logs the event. No email and no in-app notification is raised for the client — they see the change when they next open the order. - Partial and split shipments are not modelled. One store order carries one carrier, one tracking number, one ship date and one delivery date. There is no per-line-item fulfillment state.
- The separate carrier shipment register and RMA/returns register remain platform-administrator data. They are not automatically created from the client order flow. Customer and Organization mobile tracking therefore reads the authoritative, organization-scoped
MspStoreOrderlifecycle instead of this disconnected register. - Service orders carry no approval workflow.
submitauto-approves. IN_PROGRESS,CANCELLEDandREFUNDEDare not reachable on a service order. Nothing writes them. The same is true of theIN_PROGRESS,CANCELLEDandDISPUTEDfulfillment statuses, and of thestartedAt,ticketId,paymentStatus,paidAt,paymentIntentIdandinvoiceIdfields — all are written at creation or never.- A service order cannot be attributed to a managed client account. The
clientOrganizationIdmust be an organization inside the seller’s own account. For an MSP selling to a separately-accounted managed client, record the client as free-textclientName/clientEmail. - The platform fee on a manually raised service order is fixed at 12% in code. It is not read from configuration and not per-account.