Skip to content

Build REST API for the Quotation module (Fixes zerp-pk/zerp#25) - #2

Merged
HafizMMoaz merged 1 commit into
masterfrom
feat/rest-api
Jul 24, 2026
Merged

HafizMMoaz merged 1 commit into
masterfrom
feat/rest-api

Conversation

@HafizMMoaz

Copy link
Copy Markdown
Member

Fixes zerp-pk/zerp#25. Adds a Sanctum-authenticated JSON REST API for the Quotation module, backing the Flutter app, following the HRM pattern.

Endpoints (all under api/quotation, auth:sanctum)

  • GET /dashboard - counts and value by status, totals, and the 5 most recent.
  • GET /quotations - list, with customer / status / search filters, a safe sort allowlist, pagination.
  • POST /quotations - create, with line items and per-item taxes.
  • GET /quotations/{id} - show (items + taxes + customer + warehouse).
  • PUT /quotations/{id} - update (drafts only).
  • DELETE /quotations/{id} - delete (not while sent).
  • POST /quotations/{id}/sent|approve|reject - the status transitions.

Compliance with the shared patterns

  • Envelope: ApiResponseTrait {success, message, data} everywhere.
  • FormRequests extend App\Http\Requests\ApiFormRequest, so a validation failure keeps the 422 envelope. Literal-array rules.
  • Totals recalculated server-side from the submitted items; header and items written in one DB::transaction so a create/update can't half-write.
  • Visibility mirrors the web: manage-any sees the company, manage-own sees its own (a client never sees drafts). Writes require the company-owner relationship.
  • Status machine enforced: only a draft can be sent, only a sent can be approved/rejected, a sent quotation can't be deleted or edited.
  • Docs: Scramble::registerApi('quotation', ...) guarded by class_exists, at /docs/quotation.
  • No internal leakage: errors logged server-side, generic message returned.

Lessons from the earlier module APIs, applied up front

  • show/update/destroy and the status routes take an id and query with the scope, not route-model binding: these routes run bare api.json middleware (not Laravel's api group), so SubstituteBindings never runs and a type-hinted model arrives empty. An out-of-scope id 404s.
  • customer_id and warehouse_id are scoped to the caller's company, closing the cross-tenant gap the web's plain exists leaves open.

Not included (follow-up)

convert-to-invoice, create-revision, and duplicate. They reach into the accounting module and are heavier than the core CRUD this issue asks for.

Tested live against the running app

Seeded a client, warehouse, and product, then drove every endpoint with a real Sanctum token:

  • create 201 with correct server-side totals (qty 2 x 100, 10% discount, 5% tax -> subtotal 200, discount 20, tax 9, total 189), one item, one item-tax; status returns draft.
  • status machine: approve-on-draft 422; sent -> sent; delete-while-sent 422; update-while-sent 422; approve -> accepted; dashboard then shows accepted value 189.
  • update-while-draft 200 with recalculated total; show 200; show / sent on a missing id 404; delete draft 200.
  • invalid create (no items) 422; cross-tenant customer_id 422; unauthenticated 401.
  • /docs/quotation.json renders 200 with all six path groups.

Two bugs I caught and fixed while testing (why the live run matters): I had copied creator_id/created_by onto the line items, but sales_quotation_items has neither column (the model's fillable lists them, but they do not exist), so create 500'd until removed; and the create response reported status: null because status is a database default the model does not set on the instance, fixed with a refresh before serialising.

No package test suite exists here yet (zerp-pk/zerp#48), so verification is the live run above.

🤖 Generated with Claude Code

Backs the Flutter app with a JSON API mirroring the web module.

- CRUD for quotations with their line items and per-item taxes, the three
  status transitions (draft -> sent -> accepted/rejected), and a dashboard
  summary, all under api/quotation behind auth:sanctum with the shared
  {success, message, data} envelope.
- Header totals are recalculated server-side from the submitted items, and
  the header plus items are written in one DB transaction so a create or
  update cannot leave a half-written quotation.
- Same visibility as the web screen: manage-any sees the company, manage-own
  sees its own (a client never sees drafts). Writes require the company-owner
  relationship, matching the web.

Applying the lessons from the earlier module APIs:
- show/update/destroy and the status routes take an id and query with the
  scope rather than route-model binding, which does not run on these routes
  (bare api.json middleware, not Laravel's api group, so SubstituteBindings is
  absent and a type-hinted model arrives empty). An out-of-scope id 404s.
- customer_id and warehouse_id are scoped to the caller's company, closing the
  cross-tenant gap the web's plain exists rules leave open.
- Errors are logged and answered generically; no exception text reaches the client.

Not ported: convert-to-invoice, revision, and duplicate. They reach across into
the accounting module and are heavier than the core CRUD this issue asks for.
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.

[quotation] Build REST API for the Quotation module

1 participant