ModulesERP Integrations

ERP Integrations

Per-org configuration of ERP credentials so dockets can be pushed for cost reconciliation. Phase 1 supports COINS; future ERPs will plug in via the same admin UI.

ERP Integrations is the administrative surface where an organisation registers the credentials and endpoint configuration needed for Lacroo to talk to its ERP. It is the prerequisite for the Workbench’s Push to ERP workflow (shipping in Phase 3) — without configured credentials, the export pipeline will refuse to fire.

Who can configure

The following roles can change ERP integration settings and the COINS data connection (the erp.configure permission):

  • master_admin
  • organisational_manager
  • project_admin

Other members of the organisation see the COINS data status only — whether it is connected and whether each dataset is syncing — with “Managed by your admins” in place of the controls. They never see the server, the database, the views or the columns. Suppliers and field users cannot open this page. The Go API enforces the permission on every endpoint; the Next.js BFF re-checks the session before rendering the page.

Configuration walkthrough

Navigate to Org → Configuration → ERP Integrations.

Both the header and the empty-state call to action read Add ERP Connection. Clicking either opens the connection picker — a dialog with one tile per connectable ERP:

TileBehaviour
COINSOpens the COINS credential form (the only wired connector).
Viewpoint Vista, SAP S/4HANA, JD Edwards, Sage Intacct, MYOB Advanced, XeroOpens a Request this integration panel.

Add ERP Connection sets up sending goods receipts to COINS (the push settings below). Reading COINS data is connected separately, from the COINS data card; see COINS data.

A source already configured for the org renders as Connected and cannot be picked again. Once an integration is saved, the same surface lists active integrations with Edit, Test Connection, and Delete controls per row.

CSV Upload is deliberately absent from the picker. It is a cost-code sourcing mode on organizations.erp_source, not a credentialed connection, so offering it here would imply a setup flow that does not exist.

Registering interest

Confirming Register interest calls POST /v1/orgs/{orgId}/erp-integration-requests. The Go handler validates the source (rejecting coins and csv_upload with 400), then starts the ERPIntegrationRequestWorkflow Temporal workflow, which emails the Lacroo integrations inbox with the org name, the requesting admin’s own name and email, and the requested source. No ERP credential is read on this path — the service that owns it holds no cipher and its database interface has exactly two org-scoped read methods.

Nothing is persisted: the owner chose email-only, so there is no “Requested” state to read back and the response is a fixed { "requested": true }. The workflow ID is erp-interest-{orgId}-{erpSource}, so a double-clicked button cannot mail the inbox twice; a repeat click inside the retention window returns 201 rather than an error.

The endpoint reuses the existing erp.configure permission, and it is mounted unconditionally — unlike the credential CRUD surface, which only mounts when LACROO_ERP_CRED_KEY is set.

COINS form fields

The COINS connector requires the following nine fields. All are validated with Zod in the BFF and with validate struct tags in Go before persistence:

FieldDescription
entityCOINS service that consumes the message, sent as the Header entity attribute (e.g. POESB003).
test_msgtrue to flag the payload as a non-production test message in COINS; otherwise false.
user_idOriginating-system user sent as <UserID> (e.g. LACROO). Informational, not a credential.
fromSource-system tag written into the COINS payload header (e.g. LACROO_PRO).
environmentFree text. COINS environment label, written verbatim into the payload header’s <Environment> element. Type what the customer’s COINS instance expects — TEST, UAT, TRAIN, live, or a site-specific label. New forms default to TEST.
login_userService-account username COINS will authenticate against.
login_passwordService-account password. Optional on edit — leaving it blank preserves the existing stored password.
login_cidCOINS client / connection identifier paired with the login.
push_urlFull HTTPS endpoint Lacroo will POST the batch XML to.

On Add, all nine fields are required. On Edit, eight are required and login_password is optional — submitting an empty password keeps the value already stored, so admins can update non-secret fields (entity, environment, push_url) without reissuing the credential.

COINS data

COINS data is the read side of the integration. Lacroo reads cost codes, purchase order lines, subcontract lines, suppliers and jobs from views your database team builds over COINS’ SQL Server, and keeps a copy that refreshes every hour. Lacroo only reads: it never writes to these views.

Name your views and columns however you like. When you connect, you match each one to a Lacroo field, and Lacroo checks what the columns hold before anything is saved.

Connect

  1. Send the setup guide to your database team. On Org → Configuration → ERP Integrations, open the Guide tab and click Copy guide for your DBA. It lists every field below, the rules, the two addresses to allow and the sign-in options.
  2. Allow Lacroo’s two addresses on the SQL firewall, port 1433. The page lists them.
  3. On the COINS data card, click Connect data source and enter the server, the database and how Lacroo signs in: your single sign-on app, a separate Microsoft app, or a read-only SQL login. Test checks the sign-in and every mapped view.

A saved password or app secret only works for the server and the sign-in it was entered for. Changing the server, port, database, SQL user or Microsoft app asks for it again, and a new server starts its sync status afresh. Microsoft sign-in works with Azure SQL servers only (names ending in .database.windows.net).

Map fields

The Map fields step lists every dataset and every field. For each field you pick the column that holds it, and up to five values from your data are shown beside it so you can see what the column contains. These values are read live and never stored.

Picking another view for a dataset keeps every field whose column the new view also has. When a field fails a check, a Use COLUMN button offers a column from the same view that passes it.

Cost codes and PO lines must be mapped. Jobs, Subcontract lines and Suppliers are optional.

Checks

The checks run on the Map fields step and again when you save. Saving is blocked until these pass:

  • Every mapped view and column exists.
  • Company holds COINS company codes, never the contract number.
  • Jobs and PO lines give the same company for each contract they share.
  • A PO number and line item never appear twice.
  • Cost codes and PO lines are mapped.

Review then lists the checks that passed, anything worth a look (for example a recommended field that is not mapped), and which datasets sync when you save. Saving syncs the changed datasets straight away.

Only the contracts on your project stages are read. A new organisation with no stages yet is checked against the first rows of each view.

What Lacroo reads

The same list appears on the Guide tab and drives the mapper. Fields marked On GRNs: Yes are sent back to COINS on every goods receipt, so they must be right.

Cost codes

Every cost code (WBS) people can pick, per contract.

FieldWhat it isExampleUsed forNeededOn GRNs
ContractThe contract (job) number the code belongs to.3601Matched to your project stages.RequiredNo
CodeThe WBS code.3130410Picked on dockets. Sent on GRNs.RequiredYes
DescriptionWhat the code is for.Ballast supplyShown in the cost code picker.OptionalNo

PO lines

One row per line on every purchase order. Include delivered lines; Lacroo shows only open ones.

FieldWhat it isExampleUsed forNeededOn GRNs
ContractThe contract the order is raised against.4000Matched to your project stages.RequiredNo
SupplierThe supplier’s name as COINS has it.Ridgeline QuarriesMatches lines to suppliers.OptionalNo
PO numberThe purchase order number.4000-M0201Sent on GRNs.RequiredYes
Line itemThe line number on the order. Never renumbered.3Sent on GRNs.RequiredYes
DescriptionWhat the line is for.Ballast 60mmShown on docket lines.RequiredNo
RatePrice per unit.48.50Prices docket lines.OptionalNo
UnitUnit of measure.tShown on docket lines. Sent on GRNs.OptionalYes
QtyQuantity ordered.120Shows which lines are still open.RecommendedNo
Qty deliveredQuantity received so far.35Shows which lines are still open.RecommendedNo
Order valueTotal ordered value of the line.5820.00Shown in supplier rates.OptionalNo
Amount invoicedAmount invoiced so far.1697.50Shown in supplier rates.OptionalNo
CompanyThe COINS company the order belongs to. Never the contract number.400Checks the stage CID before a GRN is sent.RecommendedNo
TypeThe line type COINS uses.MShown on docket lines.OptionalNo
WBS codeThe cost code the line is charged to.3130410Shown with the PO line.OptionalNo

Subcontract lines

Same fields as PO lines, for subcontract orders.

FieldWhat it isExampleUsed forNeededOn GRNs
ContractThe contract the order is raised against.4000Matched to your project stages.RequiredNo
SupplierThe supplier’s name as COINS has it.Ridgeline QuarriesMatches lines to suppliers.OptionalNo
PO numberThe purchase order number.4000-M0201Sent on GRNs.RequiredYes
Line itemThe line number on the order. Never renumbered.3Sent on GRNs.RequiredYes
DescriptionWhat the line is for.Ballast 60mmShown on docket lines.RequiredNo
RatePrice per unit.48.50Prices docket lines.OptionalNo
UnitUnit of measure.tShown on docket lines. Sent on GRNs.OptionalYes
QtyQuantity ordered.120Shows which lines are still open.RecommendedNo
Qty deliveredQuantity received so far.35Shows which lines are still open.RecommendedNo
Order valueTotal ordered value of the line.5820.00Shown in supplier rates.OptionalNo
Amount invoicedAmount invoiced so far.1697.50Shown in supplier rates.OptionalNo
CompanyThe COINS company the order belongs to. Never the contract number.400Checks the stage CID before a GRN is sent.RecommendedNo
TypeThe line type COINS uses.MShown on docket lines.OptionalNo
WBS codeThe cost code the line is charged to.3130410Shown with the PO line.OptionalNo

Suppliers

The suppliers on each contract.

FieldWhat it isExampleUsed forNeededOn GRNs
ContractThe contract the supplier works on. Leave unmapped for one list for every contract.4000Scopes the supplier picker.OptionalNo
SupplierThe supplier’s name as COINS has it.Ridgeline QuarriesFills the supplier picker and matches PO lines.RequiredNo

Jobs

Every contract, with the COINS company it belongs to.

FieldWhat it isExampleUsed forNeededOn GRNs
ContractThe contract (job) number.4000Fills the contract picker on project stages.RequiredNo
NameThe contract’s name.Station relocationShown beside the number in the picker.OptionalNo
CompanyThe COINS company the contract belongs to. Never the contract number.400Fills the stage CID, which is sent on every GRN.RequiredYes

Rules your views follow

  • One row per purchase order line. Line numbers never change once issued.
  • Company is the COINS company the PO belongs to. Never the contract number.
  • Contract numbers match exactly what you enter on Lacroo project stages.
  • Include delivered lines. Lacroo shows only open ones.
  • Keep column names stable. If you rename one, update the mapping in Lacroo the same day.
  • Read access only. Lacroo never writes to these views.

When something breaks

What you seeWhat to do
A column is missingYour view changed. Open Fix mapping and pick the column that now holds the field.
A view is missingThe view was renamed or removed. Open Fix mapping and pick the view that holds the data.
A required field isn’t mappedOpen Fix mapping and pick the column that holds it.
Cost codes or PO lines isn’t mappedBoth are needed. Open Fix mapping and pick their views.
Company holds the contract numberThe mapped column isn’t the company code. Pick the column with values like 100 or 400.
Company doesn’t match the PO linesJobs and PO lines must give the same company for a contract. Check both Company columns hold COINS company codes.
A PO line appears twiceEach PO number and line item must appear once. Fix the view so it returns one row per line.
A stage CID doesn’t match COINSOpen the project’s stages and use the COINS value.
Can’t sign inCheck the SQL login or the single sign-on app’s secret, then Test.
Your firewall blocks LacrooAllow both Lacroo addresses on the SQL firewall, port 1433, then Test.
Can’t reach your serverCheck the server name and port, and that the database is online, then Test.
Your server took too longMake the view quicker to read for one contract, then Sync now.
A view returned too many rowsCheck the mapping points at the right view, then Sync now.
A dataset shows 0 rowsOnly contracts on your project stages are read. Add a stage with its contract, then Sync now.
Something else went wrongRun Test to see which view fails. If it still fails, contact Lacroo support.

When something breaks

When a dataset breaks, the COINS data card says why in plain words, for example “Company is mapped to CID, and lacroo.LACROO_JOBS no longer has that column. Jobs hasn’t updated since 17 Sep.” Fix mapping opens the mapper on that dataset and field, with a suggested column.

Check mapping means the last sync worked but what it read failed a check, such as Company holding the contract number.

Every break also raises one alert for the people who can configure ERP integrations. Its button reads Fix connection when Lacroo can’t sign in, reach your server or get an answer in time, and Fix mapping otherwise. It stays open while the problem lasts and clears itself after the next healthy sync. No email is sent.

Stage CIDs and GRNs

A stage’s CID is sent as <CID> on every goods receipt, and COINS refuses a CID that is not the company on the purchase order. Lacroo compares each stage CID with the companies COINS uses on that contract’s purchase orders:

  • The stages table shows “COINS uses company 400 on this contract’s purchase orders” with a Use 400 button. Nothing is changed for you.
  • The COINS data card lists the stages that need attention.
  • A goods receipt is held with CID doesn’t match COINS when a purchase order line it receipts belongs to another company than the stage CID, whatever contract that order is on. One receipt posts to one company, so a receipt that mixes orders from two companies is held too. Retry runs the same check.
  • A contract may span more than one company. On the stages table, a CID that matches any of them is accepted.

Where Lacroo has no COINS company for a contract, nothing is flagged or held.

Security note

Credentials are AES-256-GCM encrypted at rest using a master key from the LACROO_ERP_CRED_KEY env var. The COINS password is never returned in API responses; the UI shows has_password: true instead.

The encrypted JSONB lives on organizations.metadata.erp_credentials.coins, keyed by ERP source so a single org can hold multiple ERP configurations side-by-side as Phase 4 lights up additional connectors. The master key is provisioned via Fly.io secrets (production) and .env.local (development) — it is never committed to the repo and never logged.

The Go response struct for the GET endpoint deliberately omits login_password and exposes a boolean has_password flag instead. Logging middleware additionally masks any field named password, token, or secret before the log line is shipped to Axiom.

Test connection

Test Connection sends one sample line to your COINS push URL using your saved account, password and environment. The message is always flagged as a test, so nothing is posted to your ledger, even when your environment is LIVE.

If the test fails with a message about the environment, check that the environment on the form matches the COINS system the push URL points to.

Audit trail

All upserts and deletions are logged in audit_logs (events: erp_credentials_upserted, erp_credentials_deleted).

Each entry records the actor user, organisation, ERP source, and whether the operation was a create or an update. Credential values themselves are never written to the audit log — only the metadata. This satisfies SOC2 / ISO 27001 expectations around change tracking on cryptographic material without re-introducing the very leakage the encryption is meant to prevent.

Pushing dockets to ERP

Once an ERP integration is configured, the Docket Workbench (/admin/dockets) is the primary launch point for pushing dockets into COINS. The push flow is built around two non-negotiable eligibility rules and a wizard that refuses to fire if either is broken.

A docket is eligible for export if and only if:

  1. status = 'confirmed' — only fully-reviewed, supervisor-signed-off dockets can leave the system. Drafts, submitted, and reviewed dockets are blocked at the row checkbox AND at the Go API.
  2. exported_at IS NULL — the docket has not previously been pushed to any ERP. Re-pushes are intentionally blocked; if a previous push was wrong, an admin must un-push first (see below).

Both rules are enforced in three places to make accidental double-pushes structurally impossible: a SQL WHERE clause inside getErpExportableDockets, the Go eligibility.Check function called by the preview + create-batch handlers, and the React isDocketEligibleForExport predicate in the workbench. The Pushed column in the workbench shows a deep-link to /admin/erp-exports/{batchId} for already-pushed rows; non-eligible rows render a disabled-and-greyed checkbox with a tooltip explaining why.

Already-pushed rows also carry a light green row tint across the Docket Workbench (the subcontractor and materials views included), so pushed vs not-yet-pushed reads at a glance without opening the Pushed column. The tint is one step lighter than the status pills so both stay legible.

To initiate a push, select one or more eligible dockets, then click the Export ▾ dropdown above the table and choose Push to COINS. Selecting zero eligible dockets disables the menu entirely. Selecting a mix of eligible and ineligible dockets silently filters down to the eligible subset before the wizard opens — the count shown in Step 1 will reflect the filtered total.

Materials dockets: see ERP Push (Materials) for how they take a project stage and what holds them.

The wizard modal

The push experience is a single shadcn <Dialog> with three sequential steps. The dialog is escape-trapped during Step 2 (Push) so the operator cannot accidentally dismiss an in-flight workflow; closing during Step 1 or Step 3 is fine because no irreversible work is mid-flight.

Step 1 — Review

You’ll see your selected dockets aggregated per supplier, per PO line, with totals (hours, units, dollars) and a per-line warning chip when any of the following are detected: over-commits against the PO’s remaining balance, unconfirmed supplier_payment_allocations rows, missing equipment rates, or hours falling outside the contract’s award conditions. Each warning is rendered by the shared <ERPExportWarnings /> component and links back to the upstream record (allocation page, contract editor, etc.) so the operator can fix the root cause without losing wizard state.

Which supplier a line is billed to

A goods receipt carries no supplier field. COINS works out who to pay from the purchase order number on each row, so the supplier of a line is the supplier of the purchase order it is allocated to — resolved per line, not from the docket’s own supplier.

On an ordinary docket that distinction never shows: one worker, one supplier, one purchase order. On a subcontractor master it does. A master can carry workers engaged under more than one subcontractor, and each worker’s line is billed under the purchase order of the subcontractor who engaged them. Such a docket therefore appears under more than one supplier heading in Review, and the Suppliers count is the number of distinct suppliers across the batch’s lines — not the number of dockets’ own suppliers. Nothing is billed twice: the section totals sum to the docket total, unless a line is unallocated (see the third point below).

One docket producing several purchase-order rows is not a new shape — a docket has always emitted one row per allocation — and it is still one batch, one push, one receipt. A docket is never split across batches.

Three things to watch for in Review:

  • A line allocated to another supplier’s purchase order. The line stays under the purchase order it is actually allocated to, because that is what will be sent, and a warning names both the supplier who will be paid and the supplier the worker is engaged under. Re-price the docket to re-allocate it.
  • A supplier who is not on an ERP purchase order. If any supplier on the docket is paid another way (a schedule of rates, an internal rate card), the whole docket is held and the message names that supplier. Only purchase-order work can be receipted into COINS. The docket’s own supplier counts here too, not only the suppliers its lines are anchored to.
  • A line with no purchase-order line at all. An unallocated line is dropped from the receipt — there is no purchase order to receipt it against — and it is the one case where the supplier sections do not sum to the docket total. The docket carries an unallocated_line warning naming the line, and its priced total and GRN total diverge with an amber indicator. That divergence is the point: the under-bill is visible instead of being silently attributed to whichever supplier heads the docket. Fix it by giving that supplier a payment method on this project and re-pricing.

A Cancel button dismisses the dialog cleanly. A Push to COINS button creates the erp_export_batches row, kicks off the PushToERP Temporal workflow, and advances the wizard to Step 2.

Step 2 — Push

Live progress, driven exclusively by the WebSocket invalidation map for erp_export_batches (see Phase 3 Task 8 — no client-side polling). The progress card shows the workflow’s current activity (build_payload → post_to_coins → parse_response → mark_dockets_pushed) and a progress bar derived from the activity completion count. The dialog is intentionally onEscapeKeyDown and onPointerDownOutside blocked while the workflow runs because closing the modal does not cancel the workflow — Temporal continues executing in the background and the result will land in /admin/erp-exports.

When the workflow reaches a terminal state (success / failed / unknown), the wizard auto-advances to Step 3.

Step 3 — Report

A status-coloured banner summarises the outcome:

  • Success (green) — every docket landed cleanly. Per-row green ticks, total pushed count, and a View batch deep-link.
  • Failed (red) — push failed before COINS acknowledged it. Row-level error codes, the original warnings list, and a Retry button which spawns a NEW batch (the failed batch row is preserved for audit).
  • Unknown (amber) — COINS timeout or ambiguous response. Reconciliation hints are surfaced (PO refs, natural-key tokens) and the operator chooses Mark as pushed (records the batch as success retroactively) or Retry (verify-then-retry path which spawns a new batch).

The original batch is never hard-deleted on failure or unknown — it stays in erp_export_batches for forensic review.

Past batches

Navigate to Admin → ERP Exports (/admin/erp-exports) for the full history of every push attempt scoped to the current project.

The list is a URL-synced TanStack Table — every filter is reflected in the query string so a "show me only failed batches between 2026-04-01 and 2026-04-15" URL can be shared in Slack and opens with the same view for the next operator.

Filterable by:

  • Date range — from / to query params: whole days in the project’s time zone, both included (from midnight on from to the end of to).
  • Status — multi-select against the 5 lifecycle states (pending, pushing, success, failed, unknown).
  • Initiated by — operator UUID, client-side filter for v1.
  • Search — free-text against batch reference, COINS document number, and supplier name.

Click any row to drill into /admin/erp-exports/{batchId} — the batch detail page shows the per-docket roster, the original warnings list captured at push time, the COINS response payload (admin-only — masked otherwise), and admin-only un-push controls.

Export notifications

Lacroo emails when an export fails or finishes in the unknown state. A clean push sends nothing — it is already visible on this list and in the workbench, and mailing a receipt for work that went exactly as asked is how one project ended up with 25 people on every push.

The person who ran the export always gets the email. Everyone else is chosen per project from Admin → ERP Exports → Notifications, which offers the project’s office staff (the same office/field split as the Issue notification pickers — field workers and subcontractor crew are not offered).

Three states, and the first two are not the same thing:

RecipientsWho is emailed
Never setThe person who ran it, plus every active full-access member of the project — resolved at send time, so it keeps up with role changes. This is where every project starts.
Set, nobody tickedThe person who ran it, and nobody else.
Set, people tickedThe person who ran it, plus exactly those people.

The dialog’s left-hand button moves between the two rows the tick boxes cannot express. On a project that has never been set it reads Notify only whoever runs it and writes the middle row in one click. Once recipients are set it reads Notify everyone with full access and returns the project to the first row — undoing the choice, not clearing the list. Save stays disabled until something actually changes, so opening the dialog to look cannot cut the cohort off.

Backed by GET / PUT /v1/orgs/{orgId}/projects/{projectId}/erp-exports/notify-settings, gated on the same erp.export permission as pushing. The list is stored on projects.erp_outcome_notify_users; null there is the “never set” row above, which is why the API returns user_ids: null alongside configured: false rather than an empty array.

Un-push (admin-only)

Un-push is the explicit “I made a mistake, give me back the dockets” affordance. It is destructive in the local sense only — it does NOT call COINS, it does NOT cancel anything in the ERP, and it is your responsibility to manually reconcile in COINS afterwards.

What un-push does:

  1. Clears dockets.exported_at, dockets.last_export_batch_id, and the dockets.export_status markers on the affected rows.
  2. Writes an audit_logs entry (erp_docket_unpushed) capturing the actor, batch, and the docket UUIDs.
  3. Raises a system_alerts row with severity info so the cost team is told “an admin un-pushed N dockets — please verify the COINS-side state matches”.
  4. Makes the dockets eligible for export again — they re-appear in the workbench with checkable rows.

What un-push does not do:

  • It does not touch COINS. If the original push landed, the records are still in COINS and need to be voided / reversed by a finance user with COINS access. The system_alerts row exists precisely to make this human reconciliation step impossible to forget.
  • It does not delete the original erp_export_batches row. The batch stays in history with its full payload and outcome; only the per-docket markers are cleared.

Two flavours are available on the batch detail page: per-row Un-push buttons (admin-only, success-only) and a bulk Un-push all dockets button at the top of the batch. Both go through POST /v1/orgs/.../erp-exports/{batchId}/unpush which the Go API role-gates to admins.

Verifying unknown-state batches

Network timeouts and partial COINS responses produce an unknown status — the workflow finished but Lacroo cannot prove whether COINS accepted the payload or not. This is the only manual-judgement state in the pipeline.

The Step 3 Report (and the /admin/erp-exports/{batchId} detail page) surfaces two admin-only choices:

  • Mark as pushed — POST verifyERPExportBatch with resolution: "confirm_pushed". Use this once you (or finance) have logged into COINS and visually confirmed the dockets are present. The batch flips to success, exported_at is stamped on the dockets, and no further action is taken.
  • Retry — POST verifyERPExportBatch with resolution: "retry". Spawns a NEW batch (same docket set), re-runs the PushToERP workflow, and re-enters the wizard at Step 2. Use this when COINS is confirmed empty / the previous attempt clearly never landed.

Reconciliation hints (PO refs, natural-key tokens captured during the failed push) are rendered above the choice buttons to make the COINS-side check faster.

Permissions

RoleView batchesInitiate pushRetry / verifyConfigure ERP credentialsUn-push
master_adminyesyesyesyesyes
organisational_manageryesyesyesyesyes
project_adminyesyesyesyesyes
project_manageryesyesyesnono
engineeryesyesyesnono
supervisornonononono
Field users / suppliersnonononono

The role-gates are enforced at three layers: the Go API’s canExportERP admit-list middleware, the Next.js Server Actions (which re-check the Better Auth session before proxying), and the React UI (which hides controls for non-eligible roles to avoid the “button visible but click 403s” footgun). The credential-configuration surface is gated by erp.configure, which master_admin, organisational_manager and project_admin hold; only they can read or mutate organizations.metadata.erp_credentials.* or the COINS data connection.

Forward-looking

v1 supports COINS only. The architecture is deliberately interface-ready for additional ERPs — erp_export_batches.erp_source is a string column, the credential JSONB is keyed by ERP source (erp_credentials.coins, future erp_credentials.sap, etc.), the PushToERP Temporal workflow takes the source as a parameter and dispatches to per-source activities, and the Workbench’s Export ▾ menu is structured to render one menu item per configured ERP integration. Connectors for SAP, Vista, JD Edwards, Microsoft Dynamics, and Epicor are on the Phase 4 roadmap and will reuse 100% of the wizard, batch tracking, un-push, and verify infrastructure shipped in Phase 3 — only the per-source activity (payload format + transport) needs to be authored.

Guided ERP supplier setup

When a project admin links a supplier to a project and selects an ERP payment method — either ERP Purchase Order or ERP Subcontract Order — the add-supplier wizard gains a dedicated ERP reference step. This step is only shown for those two payment method types; non-ERP methods (internal rate card, manual, etc.) skip it entirely.

Web admin only. This flow is not available on mobile or in the supplier portal.

The ERP reference step

After choosing the agreement type and payment method, the wizard inserts an ERP Reference step before Finish. This step is optional — click Skip to add the supplier now and link the ERP reference (and import rates) later from the supplier’s admin page; skipping runs no fetch and creates no contract.

To link the ERP reference now:

  1. Enter the ERP reference (the COINS Purchase Order number or Subcontract Order number that this supplier link maps to).
  2. Click Continue. The wizard immediately kicks off a background fetch of rate and cost lines from the ERP for that reference. A status banner updates live — no page reload required:
    • Fetching rates… — the Temporal workflow is running.
    • Found N rate lines — the fetch completed and returned lines.
    • No lines found or an error banner — the fetch returned nothing or the ERP returned an error.

The fetch is durable: it runs as a Temporal workflow so a temporary ERP outage or network blip will be retried automatically. Progress is pushed to the browser over the existing WebSocket connection (Postgres LISTEN/NOTIFY → Go → TanStack Query invalidation) — there is no polling.

The COINS supplier name

A supplier’s lines are read from COINS by its COINS supplier name, matched exactly (spaces at either end and capitals aside). “Connect Resources” and “Connect Resources Pty Ltd” are two different suppliers to COINS.

  • Saving a name. The name you pick or type is checked against the suppliers COINS has on the project’s jobs. A name COINS doesn’t have there is refused: pick one from the list. When the list can’t be read (the project’s stages have no job numbers, COINS has no suppliers on those jobs, or COINS data isn’t connected yet) the name is saved with a warning instead.
  • On the supplier’s Rate & Cost Data page. When the saved name isn’t a COINS supplier on the project’s jobs, a COINS supplier not found notice shows with a Pick the supplier button that opens the same COINS picker.
  • Refresh from ERP. A Refresh under a name COINS doesn’t have reads nothing and changes nothing. It is listed in Recent Syncs as 0 matched — check the ERP supplier name, not as a success. A name COINS has, with no open lines, is still a normal sync.

Create an active starter contract

When the fetch returns at least one rate line, a Create an active starter contract checkbox appears on the Finish step (checked by default). When you click Finish with the checkbox on:

  • A new contract is created for the supplier on this project, using the agreement type selected earlier in the wizard (Labour Hire, Plant Hire, Subcontractor, or Materials).
  • The fetched rate lines are imported into the contract automatically.
  • The contract status is set to Active immediately — no separate activation step is needed.
  • The wizard closes and you are taken directly to the contract editor, where the imported rate lines are already visible and ready for cost allocation.

The operation is idempotent: clicking Finish twice (e.g. after a network drop) will not create a second contract.

Fallbacks

ScenarioOutcome
Fetch returns zero linesThe Create starter contract checkbox is hidden. The supplier is still added; the contract can be created manually afterwards from Admin → Contracts → + New Contract.
Fetch fails (ERP unreachable, auth error)An error banner is shown on the Finish step. The checkbox is hidden. The supplier is still added; you can retry from Admin → Contracts once the ERP issue is resolved.
Supplier name isn’t in COINS0 matched — check the ERP supplier name is shown. Nothing was imported. Set the COINS supplier name on the supplier’s Rate & Cost Data page, then refresh.
Checkbox unchecked by the adminThe supplier is added without a contract. A contract can be created manually later.

Permissions

Creating the starter contract requires the contracts management permission (contracts_manage) — the same permission required to create any contract. In practice this means master_admin, organisational_manager, and project_admin roles can trigger it (the bAdmin3 preset in the authz layer). Admins without this permission can still add the supplier through the wizard but cannot auto-create the contract; the contract must be created separately by a user with contracts_manage.

The ERP credentials used for the fetch are the org-level COINS credentials configured under Org → Configuration → ERP Integrations — see Configuration walkthrough above.

Spec & plan

  • Spec: docs/superpowers/specs/2026-04-26-grn-erp-export-design.md
  • Plan (Phase 1): docs/superpowers/plans/2026-04-26-grn-erp-export-phase-1.md
  • Plan (Phase 3): docs/superpowers/plans/2026-04-26-grn-erp-export-phase-3.md