TradeForce migration docs

#Contractor payments and payouts

Module id: MOD-PAYMENTS
Doc profile: capability

Contractor-facing payment schedules and history on /contractor/payments: This week totals, Previous / Upcoming tabs over sent and company-approved time entries, a shift details popup, and a bounded Stripe account affordance. Programme fence MON-MARKETPLACE blocks Connect onboarding, KYC, live balance fetch, and payouts until answered. Admin payment-schedule editors and paid-status writes stay MOD-ADMIN; this module owns the contractor read surface and the Upcoming payment cycle PostMark outbound contract.

#Purpose

Give signed-in contractors one place to see what they are owed and what was already paid for shifts they tracked, open a row’s shift details, and (when an account id exists) open the Stripe dashboard. An upcoming-cycle email can land them on the Upcoming tab. Money writes and Connect account creation are out of scope here — schedules are read models under RLS; Stripe Connect plugins on the source page are fenced, not ported.

#Scope boundaries

In scope Out of scope
/contractor/payments cabinet body: overview, tabs, payment rows, details popup Workspace Header / SideBar chrome (MOD-SHELL)
Read models on time_entries (← time_tracking1) with contractor-scoped RLS Client-authoritative money writes (MON-CLIENT-WRITES / LOG-CLIENT-WRITES)
profiles.stripe_account_id / stripe_name display + dashboard URL when id present Stripe Connect plugin create/login, KYC, writing stripe fields from Connect, live GetDataFromAPI balance (MON-MARKETPLACE)
Edge send for Upcoming payment cycle (PostMark) + deep-link landing on Upcoming tab Admin UI that marks paid and invokes the connector today (MOD-ADMIN callers)
Optional ?tab=previous_payments / ?tab=upcoming_payment Inventing a live rates table (all fields deleted in export)
PageLoaded role gates (anon → /, admin → admin, company → company shifts) Company Payments tab body / admin Payments tab (other modules)

#Actors

Actor Behavior in this module
Contractor Opens payments; switches Previous / Upcoming; reviews This week sum and rows; opens Details; uses Stripe control when allowed.
Session client Anon-key Supabase reads under RLS; never ships PostMark or Stripe secret keys.
Cycle notifier (edge) Service/admin-authorized PostMark template send; failures mapped, not raw upstream bodies.
Admin (caller only) Source admin workflows invoke the PostMark call after marking paid — implement UI stays MOD-ADMIN; this module owns the outbound contract.

#Capability composition

One contractor cabinet route hosts overview cards, schedule tabs, and the repeating payment list. Stripe control and balance label sit in the title/overview row but Connect and live balance stay fenced. Cycle email is an edge outbound that deep-links back into the same route’s Upcoming tab.

flowchart TB subgraph page["Contractor payments"] Overview["This week + Your balance label"] Tabs["Previous / Upcoming tabs"] List["Payment rows"] Popup["Shift details popup"] Stripe["Stripe account control"] end subgraph data["Read models"] Entries["time_entries"] Profiles["profiles stripe fields"] end subgraph outbound["Outbound"] Edge["upcoming-payment-cycle edge"] PM["PostMark withTemplate"] end Overview --> Entries Tabs --> List List --> Entries List --> Popup Stripe --> Profiles Edge --> PM Edge -->|"tab=upcoming_payment"| Tabs

Bubble evidence for page, workflows, option sets, and connector is under Traceability / IR evidence.

#Primary journeys

Journeys are reconstructed from contractor_payment PageLoaded and ButtonClicked workflows, RG search states, and the PostMark call Upcoming payment cycle (for contractor). Connect plugin workflows exist on the Stripe control but are not delivered until MON-MARKETPLACE is answered.

#J1 — Contractor reviews payment schedules

  1. A signed-in contractor opens /contractor/payments (optionally with ?tab=previous_payments or ?tab=upcoming_payment).
  2. Signed-out visitors redirect to /; signed-in admin redirects to admin; signed-in company redirects to company shifts — only contractors remain on the page.
  3. The page shows heading “Payments”, overview “This week” with a $ sum of paid_amount for the contractor’s sent rows whose related shift date falls in the current ISO week, and tabs defaulting to Previous payments.
  4. Previous tab lists sent + company-approved rows with payment_status = paid; Upcoming lists the same filters with payment_status not equal to paid; empty filter shows “No result found”.
  5. Each row shows Company, Payment ($ × logged hours including minutes/60 × shift hours required), Shift date, and Status (“Paid” or “Unpaid” when Upcoming is selected).
  6. Details opens the shift details popup (Company Name, Job position, Shift date, Total of payment via skill-matched company contractor rate × hours, Hours worked, Job location, feedback or “No feedback”); Close hides and resets the popup.
  7. Load failures show a user-visible error — not a blank success state. No contractor money writes on this surface.

#J2 — Contractor uses evidenced Stripe account fields

  1. On /contractor/payments, the Stripe control shows “View stripe account” when profiles.stripe_account_id is non-empty, and “Connect Stripe account” when empty.
  2. When an id is present, click opens https://dashboard.stripe.com/dashboard in a new tab.
  3. When empty, click must not run Connect plugin actions or persist new stripe fields — show a pending/unavailable affordance until MON-MARKETPLACE is answered.
  4. “Your balance” label may render; live Stripe balance via connected-account header is out of scope — use empty formatting $0 until the programme unblocks it.

#J3 — Upcoming payment cycle email lands on Upcoming tab

  1. An authorized admin/service caller invokes the upcoming-payment-cycle edge (source admin workflows mark paid then call PostMark; admin UI remains MOD-ADMIN).
  2. The edge POSTs PostMark email/withTemplate with server-only secrets, TemplateAlias evidenced as user-invitation-3, To = contractor email, and empty TemplateModel as exported — timeout and idempotency enforced.
  3. Product landing contract: the contractor opens /contractor/payments?tab=upcoming_payment and sees the Upcoming schedule from J1 (whether that URL is already inside the PostMark template body is HQ-1).
  4. Missing secrets or PostMark failures fail closed with a mapped error; no token leaks to the client.

#Vertical slices (decompose)

Item Product outcome
PAY-SCHEDULES Contractor payments page: This week sum, Previous/Upcoming tabs, payment rows, details popup, role gates, time_entries read RLS.
PAY-STRIPE-FIELDS Stripe control copy + dashboard URL when account id present; profiles stripe columns; Connect/balance fenced.
PAY-CYCLE-EMAIL Edge PostMark upcoming-cycle send + deep-link landing on Upcoming tab.

#Key UI states

State When User sees
Previous tab (default) tab_ = previous_payments Paid rows; Status “Paid”
Upcoming tab tab_ = upcoming_payment Unpaid rows; Status “Unpaid”
Empty list Filtered search empty “No result found”
Details open Details clicked Shift details popup for the row
Stripe linked stripe_account_id non-empty “View stripe account”
Stripe unlinked stripe_account_id empty “Connect Stripe account” (Connect flow blocked)
Balance empty No live balance (fence) “Your balance” with $0 empty formatting
Wrong role / signed out PageLoaded gates Redirect away from payments

#Style and fidelity

Cabinet workspace outlet on /contractor/payments. Transcribe visible copy (“Payments”, “This week”, “Previous payments”, “Upcoming payment schedule”, column headers, “Details”, “No result found”, popup labels, Stripe control strings). Stamp data-bubble-id on page and payment-region roots. Preserve RG column hierarchy — do not restyle into a marketing card grid. Shared tokens come from the app-level stylesheet that SHELL-LAYOUT (MOD-SHELL) delivers out of IR styles (ir/slices/styles____part_all.json — 49 styles, 8 color tokens, 1 font token); this cycle's module map has no separate design-system module, so do not defer a token to one. Responsive element conditions at narrow widths stay structural tweaks, not a different product layout.

#Failure modes

Case Expected behavior
Signed-out visit Redirect to /; no payment UI.
Admin or company visit Redirect to admin or company shifts respectively.
Schedule / This week read failure Visible error; do not pretend empty success.
Empty Previous or Upcoming filter “No result found”.
Details with missing shift relation Show available fields; do not invent job data.
Stripe dashboard open failure User-visible error; stay on page.
Stripe id empty click No Connect plugin; no stripe field writes; pending affordance.
Cycle email missing secret Fail closed; mapped domain error.
PostMark upstream failure Mapped unavailable error; no raw body to client.
Anon read of another contractor’s rows RLS deny.

#Needs human clarification

Id Question Why unclear (plain + IR) Impact
HQ-1 Should the Upcoming payment cycle email deep-link to /contractor/payments?tab=upcoming_payment, and should TemplateModel gain fields, or does PostMark template user-invitation-3 already encode the destination? Plain: Bubble admin call sites send TemplateAlias user-invitation-3 with an empty TemplateModel and only interpolate To (contractor email). The template HTML lives in PostMark, not in the export, so whether contractors already get a payments URL is unknown. Decompose PAY-CYCLE-EMAIL assumes a payments deep link for landing behaviour. IR: external_call:bTNMZ:bTNMt, admin actions bTNNo / bTNNp. blocks_implement

#Traceability

Product concept Target (route / component / table) Bubble evidence
Contractor payments page /contractor/payments, ContractorPaymentsPage bTIzV0
Payment column layout G-payment region bTIzz0
Heading “Payments” page title bTJAH0
This week sum overview card → time_entries.paid_amount bTJAp0, paid___number
Your balance label / fence overview card; no live Connect balance bTJBb0, bTJBg0, bTJBh0
Stripe control copy StripeAccountControl bTJAS0, element_condition:bTJAS0:1
Open Stripe dashboard new-tab URL bTMLD, bTMLL
Connect plugins (fenced) not implemented bTMFn, bTNKQ0, bTNJn0, bTNJs0, bTNJt0, bTNJx0, bTNJy0
Previous / Upcoming tabs tab_ + tabs option bTJBr0, custom_state:bTJBr0:tab_, bTJBs0, bTJBx0, bTJBt0, bTJBy0, bTJCn0, bTJCo0, tabs
Set Previous tab SetCustomState bTMLQ, bTMOK
Set Upcoming tab SetCustomState bTMOL, bTMOR
Payment rows RG PaymentRows bTJCQ0, bTJCR0
Row payment amount logged hours×hours_required bTJCW0
Empty copy “No result found” bTJCi0
Status Paid / Unpaid row status text bTJCb0
Details → popup show + display data bTJCd0, bTJCh0, bTMOW, bTMOc, bTMOd
Details popup PaymentDetailsPopup bTJDB0
Close popup hide + reset bTJFc0, bTMOh, bTMOn, bTMOo
Signed-out redirect → index bTNMB2, bTNMD2
Role redirects → admin / company shifts bTNMH2, bTNMJ2, bTNMO2
Payment status enum status_paid / payment_status status_paid, bTMpW, status_option_status_paid
Time entry fields time_entries columns time_tracking1, send_boolean, approve_company_boolean, contractor_user, logged__hours__number, logged__minuts__number, notes_company_text
Stripe profile fields profiles.stripe_* user, stripe_account_id_text, stripe_name_text
Dead rates type omit table rates (residual dead)
Upcoming cycle PostMark edge + connector external_ns:bTNMZ, external_call:bTNMZ:bTNMt
Admin invoke sites (caller) MOD-ADMIN bTNNo, bTNNp
Time entry privacy RLS on time_entries privacy_role:time_tracking1:contractor_, privacy_role:time_tracking1:company_, privacy_role:time_tracking1:admin_, privacy_role:time_tracking1:everyone

#IR evidence

#Module evidence roots

Supporting segments: ir/slices/bTIzW0____root.json, user, rates (dead), status_paid, external_ns:bTNMZ.

#Path-key ↔ inventory id aliases

Inventory id Export path key
bTIzV0 /pages/bTIzW0
external_ns:bTNMZ /settings/client_safe/apiconnector2/bTNMZ
external_call:bTNMZ:bTNMt /settings/client_safe/apiconnector2/bTNMZ/calls/bTNMt

#PageLoaded gates (chased)

#Schedule filters (chased)

#This week sum (chased)

#Row payment amount (chased)

#Stripe control (chased)

#PostMark upcoming cycle (chased)

#Residual dispositions

#Privacy & authorization (time_tracking1time_entries)

This module’s SPA only needs contractor-scoped reads on /contractor/payments. Target: RLS so authenticated contractors read only own contractor_id rows; no contractor update of money fields here. Company/admin writers stay on other modules with server authority (MON-CLIENT-WRITES).

Generated from the repository at 2026-09-22 15:55 UTC · source of truth is docs/ in the workspace, not this site.