Skip to content

v0.6.50#251

Merged
roncodes merged 36 commits into
mainfrom
dev-v0.6.50
May 27, 2026
Merged

v0.6.50#251
roncodes merged 36 commits into
mainfrom
dev-v0.6.50

Conversation

@roncodes
Copy link
Copy Markdown
Member

No description provided.

roncodes and others added 29 commits May 21, 2026 15:57
Introduces a B2C customer surface directly in FleetOps so portals can
authenticate end-customers with the existing flb_live_… API credential —
no Storefront publishable-key + Store/Network coupling required.

Endpoints (all under /v1/customers/...):

Public (API credential only):
  POST request-creation-code   send email/SMS verification code
  POST /                       create Contact+User after verifying code
  POST login                   email/phone + password → Sanctum token
  POST login-with-sms          send login code (SMS, falls back to email)
  POST verify-code             verify code → Sanctum token
  POST forgot-password         send reset code
  POST reset-password          verify reset code + set new password

Authenticated (require Customer-Token):
  GET  me / PUT me             profile read/update (mirrors to linked User)
  POST logout / logout-all     revoke current / all tokens for this user
  GET  orders                  scoped Order::where('customer_uuid', …)
  POST orders                  create freight order with customer_uuid set
  GET  orders/{id}             owner-checked order detail
  GET  places                  customer's saved Places
  POST register-device         push-token registration for linked User

Implementation:
- Tokens are Sanctum PersonalAccessToken with `name` = Contact UUID
  (matches Storefront convention so SDKs and headers are interchangeable).
- AuthenticateCustomerToken middleware verifies the Customer-Token header
  and cross-checks the resolved Contact's company_uuid against the API
  credential's session('company'); 401/403 on mismatch.
- CustomerAuth helper resolves the token with a company-preferred
  fallback for the multi-company edge case.
- Customer model is a thin Contact specialization with type=customer.
- Verification slugs are fleetops_* (create_customer, customer_login,
  customer_password_reset) — no Storefront slugs reused.
- No new tables, no migrations: contacts.email/phone/user_uuid,
  orders.customer_uuid, and personal_access_tokens already cover it.
- OAuth providers (Apple/Google/Facebook) deferred to a follow-up.

Static-shape tests live in server/tests/CustomerEndpointTest.php and
follow the package's existing pest convention. End-to-end HTTP tests
belong in the parent api/ harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs in the freshly-added v1/customers endpoints surfaced on
first live test (HAR shows: 400 from /v1/customers/request-creation-code
with "Attempt to read property 'name' on null (View:
.../mail/verification.blade.php)").

Bug 1: requestCreationCode passed an unsaved Contact as the verification
subject, so the morphTo subject_uuid was null. The verification mail
template references {{ $user->name }} via the morphTo relation, which
resolves null → fatal in blade.

Fix: look up an existing User by identity, or stub-create one with
`name = "Pending Customer"`. The stub gives the mail renderer a real
record to greet, and create() backfills name + password on verification.

Bug 2: `password` and `type` are guarded on the User model, so
`User::create([... 'password' => ..., 'type' => 'customer'])` silently
dropped both fields. Customers created via signup would therefore have
no password and no type — login would fail.

Fix: assign password via `$user->password = $plaintext` (the model's
setPasswordAttribute mutator hashes) and set type via `setUserType()`.
Also stop double-hashing in resetPassword (was Hash::make then mutator).

Also makes create() idempotent on the Contact: reuse the existing
customer-Contact for (user, company) instead of crashing on the second
signup attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The signup form already collected the customer's name (and phone) by the
time it asked for a verification code. Plumb those through so:
  - the verification email greets the customer by their real name
  - the pre-created User row holds real values instead of the
    "Pending Customer" placeholder
  - create() doesn't need to overwrite stub values on confirmation

VerifyCreateCustomerRequest now accepts optional `name` and `phone`;
requestCreationCode uses them when stub-creating the User, and refreshes
the row's name when an existing pending stub is re-prompted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When `POST /v1/customers` receives a home-address payload (either as a
top-level `address` object or nested under `meta.address`), create a
Place record:
  - company_uuid = the API credential's company
  - owner_uuid / owner_type = the new customer Contact (polymorphic)
  - type = "residential"
  - Both Storefront-style (street1/province/postal_code) and
    portal-form-style (line1/state/zip) keys are accepted.

Then link the Place to the customer via Contact.place_uuid so it appears
as the default address and `GET /v1/customers/places` returns it.

Idempotent: only creates a Place when the Contact has no place_uuid set
and the payload has at least one usable address field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is a public Fleetbase API surface — it must follow established model
field shapes, not invent client-specific aliases. Audited and removed
every divergence I'd introduced:

- `create()` and the `place` field — drop the line1/line2/state/zip
  aliases. Accept only the canonical Place fillable shape (street1,
  street2, city, province, postal_code, neighborhood, district, building,
  country, phone, meta) or an existing Place public_id. Field whitelist
  via `array_intersect_key` so unknown keys are silently ignored.

- Drop `meta.origin = 'fleetops_customer_portal'` invention from
  create(), login(), and verifyCode(). Storefront uses meta.storefront_id
  legitimately; FleetOps customers don't need a portal-specific tag.

- `createOrder()` — full rewrite to mirror `OrderController::create`'s
  canonical Order shape. Accepts the same fields the operator API does:
  type / order_config / scheduled_at / notes / meta / internal_id, and
  either a `payload` (object or public_id) or top-level pickup / dropoff /
  return / waypoints / entities. No top-level `item`, `weight`, `value`,
  `mode`, `delivery`, or `category` aliases — clients translate their
  form shapes into entities + meta before calling.

  The customer endpoint *forces* customer_uuid from the Customer-Token,
  status='created', and ignores any client-supplied customer / driver /
  vehicle / facilitator / dispatch fields. Payload-building delegates to
  `Payload::setPickup` / `setDropoff` / `setEntities` so customer-created
  orders are indistinguishable from operator-created ones at the data
  layer.

- CreateCustomerOrderRequest validates only the canonical fields.

- CustomerEndpointTest gains a "no client-portal field aliases" assertion
  that fails the build if any of those forbidden patterns reappear in
  the controller source, plus a "createOrder mirrors the canonical
  Fleet-Ops order shape" assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the principle explicit at the only meta write that future contributors
might be tempted to touch: customer creation. `meta` is client-owned;
controller-side stamps are only justified when the backend itself reads
that data back (e.g. Storefront's `meta.storefront_id` for query scoping).
The customer surface has no such backend need.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additive, canonical extensions to the public Fleet-Ops API surface,
both convention-aligned with existing resources:

1. OrderConfigs as a first-class read-only public resource (mirrors how
   Places, Vendors, Contacts, Orders are already exposed):

     GET /v1/order-configs        — list configs for the company
     GET /v1/order-configs/{id}   — find by uuid|public_id|namespace|key

   `find()` defers to `OrderConfig::resolveFromIdentifier()` so callers
   can use `/transport`, the namespace, the public_id, or the uuid.

   A new `v1/OrderConfig` resource projects only the public-safe shape:
   id, key, name, namespace, description, tags, status, version, and the
   activity `flow[]` carrying `{code, status, details, color, complete,
   pod_method, require_pod}`. Internal-only fields (raw entities JSON,
   flow logic blocks) are filtered out so the public response is small,
   safe, and useful for drivers/portals/integrations that need to render
   status chips and activity labels from the canonical config.

2. `company` sub-object on the `Customer` resource (returned by
   /v1/customers/me, /login, /signup, /verify-code, etc.). Resolves
   currency through the existing canonical helper
   `Utils::getCompanyTransactionCurrency()` which already does the
   `companies.currency` → ledger `base_currency` → "USD" chain. The
   sub-object exposes id, name, currency, country, phone — same fields
   any caller could already discover via other channels, just bundled
   conveniently so authenticated customer apps don't need a separate
   request to render currency labels or contact info.

No new write surface, no new auth requirements, no client-portal
aliases. Both endpoints require only the public Fleet-Ops API key, like
the existing /v1/tracking-numbers/{n} public read endpoint.

Static-shape tests assert:
  - order-configs routes register both methods
  - OrderConfigController exposes only read-only methods (no create/
    update/delete on the public surface)
  - OrderConfig resource emits `flow[]` with the canonical keys and
    never exposes the raw entities JSON
  - Customer resource exposes `company` and uses
    `Utils::getCompanyTransactionCurrency`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional `service_quote` field to the customer order create
endpoint, matching how `OrderController::create` resolves quotes:

  - CreateCustomerOrderRequest accepts a `service_quote` string (uuid or
    `sqte_…` public_id).
  - `createOrder()` resolves it via `ServiceQuote::resolveFromRequest`
    and calls `$order->purchaseServiceQuote()` after creation so the
    PurchaseRate is locked onto the order with the quoted pricing.

This lets customer portals pull live quotes from
`GET /v1/service-quotes`, present them to the customer, and submit the
chosen one when creating the order — same flow operators use.

No write-surface changes beyond accepting one more optional field; all
existing tests + canonical-shape assertions still hold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add public customer API + OrderConfig resource + Customer.company sub-object
…rvice-rates

Add multi-zone distance service rates
@roncodes roncodes merged commit 0b01ef4 into main May 27, 2026
7 checks passed
@roncodes roncodes deleted the dev-v0.6.50 branch May 27, 2026 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant