Skip to content

Jobs

Location: dg_smart_pos_api/modules/Jobs. A generic, configurable job/work-order engine, deliberately not specific to any trade: a JobType (e.g. “Phone Repair”, “Custom Fabrication”, “Batch Bake”) defines a flavor (service/fabrication/production, label-only, see below) and a tree of custom properties, and a Job against that type accumulates tasks, materials, labour, expenses, and revenue charges before being invoiced into a real Sales Sale. This is a completely separate system from the Repair module’s own RepairJobCard models; the two do not share code, but JobInvoicingService::invoice()‘s own docblock says it structurally mirrors RepairPartService::collect()/sellParts() and Repair’s “Collect” action (one click folds a status transition and settlement into a single action) as “the only working precedent in this codebase for this.” JobLabour.employee_id belongs to HR’s Employee model directly, Job.customer_id belongs to SaleCustomers’s SaleCustomer, and App\Services\Analytics\ServiceRevenueAnalyticsRepository combines Jobs’ and Repair’s labour/service revenue as two separate, parallel sources into one P&L view, rather than the two modules sharing a revenue table.

Jobs is enabled in config/modules.php’s enabled array, and its migrations are correctly tenant-scoped (listed in ModulesServiceProvider::$tenantOnlyMigrationModules), so the schema exists on every tenant DB even though the API surface doesn’t currently respond.

  • Job - the work order itself: job_number (JOB-{store_id}-{00001}, see below), job_type_id, customer_id, source_type/source_id (where it came from), title/description, status, received_by/assigned_to, estimated_amount, invoiced_sale_id, output_stock_id/output_quantity, and the lifecycle timestamps received_at/promised_at/completed_at/invoiced_at/closed_at. Carries computed, not stored, estimatedCost(), actualCost(), revenue(), profit(), margin(), paidTotal(), balance().
  • JobType / JobTypeProperty - a type has a flavor and a default_output_tracks_stock flag; properties form a tree (parent_property_id) of kind (boolean/text/select/number) fields, where only a boolean property may have follow-up children (a “Yes/No” question that reveals more fields).
  • JobPropertyValue - the answers for one job’s property tree.
  • JobTask - a checklist item on the job (status: pending/in_progress/done/skipped, assigned_to, due_at).
  • JobMaterial - a stock/product-variant line with a lifecycle of its own: estimated_quantity/estimated_unit_cost at planning time, actual_quantity/actual cost once consumed, plus returnToStock/writeOff as terminal actions. A STATUS_PLANNED line can be deleted outright; a consumed one cannot.
  • JobLabour - employee_id (HR’s Employee) or a free-text worker_name, entry_type (hours/fixed), estimated vs. actual hours/rate/amount.
  • JobExpense - a simple actual-only cost line (category, amount, incurred_at); unlike materials/labour it has no estimated counterpart.
  • JobCharge - a revenue line (description, stock_id/product_variant_id, charge_type, quantity, unit_price, discount, tax). The moment any charge row exists for a job, charges become the authoritative revenue source over estimated_amount.
  • JobPayment - a deposit/part-payment taken before final invoicing (cash/mpesa/bank/total_payment), separate from the Sale payment created at JobInvoiceController::store.
  • JobStatusHistory - one row per status transition, written by JobStatusService.
  • JobCounter - one row per store, last_number, row-locked (lockForUpdate) inside a transaction by JobNumberService::next() to serialize job_number generation; backstopped by a unique(store_id, job_number) index.
  • JobAttachment - an uploaded file (kind), served through JobAttachmentService::url().

JobStatus (draft -> requested -> quoted/approved -> in_progress -> on_hold/completed -> invoiced -> closed, plus cancelled reachable from most non-terminal states) is one shared graph across every flavor; the enum’s own docblock says flavor must never branch it. requested -> in_progress is allowed directly so a simple job can skip formal quote/approval. JobController::transition validates the target against JobStatus::canTransitionTo() via JobStatusService, which also writes the JobStatusHistory row.

JobController::store is a fast-create path: an existing customer_id or a customer array (name/phone/email) to create one inline, optional job_type_id, property_values answering the type’s property tree, and an optional initial_payment recorded in the same call. update refuses to touch a job whose status is terminal (isTerminal(), i.e. closed/cancelled).

Materials: plan, then consume, return, or write off

Section titled “Materials: plan, then consume, return, or write off”

JobMaterialController::store (-> JobMaterialService::addPlanned) only records the planned line; nothing is deducted from stock yet. consume records the actual quantity taken (optionally overriding unit cost); returnToStock and writeOff are the two ways a planned-but-unused or consumed-but-unusable line gets closed out. destroy only works while the line is still STATUS_PLANNED.

JobInvoiceController::store -> JobInvoicingService::invoice(). Locks the Job row, rejects a job that’s already been invoiced (invoiced_sale_id set) or isn’t in in_progress/on_hold/completed, auto-transitions it to completed first if it isn’t already (so that hop still lands in JobStatusHistory even though the caller only took one action), then invoices from job_charges if any exist, or a single line derived from estimated_amount otherwise. Tendered cash+mpesa cannot exceed the outstanding balance unless waive_balance is set, and only a caller with an admin/super-admin role can waive it. This goes through SalesService, the same service Sales itself uses to create a Sale.

JobMaterialController, JobTaskController, JobLabourController, JobExpenseController, JobChargeController, JobAttachmentController, and JobPaymentController all use the ScopesToStore trait: they resolve the parent Job scoped to {store} first, then confirm the nested resource (task/material/labour/…) actually belongs to that job before acting on it, specifically because a route-model-bound nested id alone doesn’t prove it belongs to the store in the URL.

Defined in modules/Jobs/Routes/jobs.php under auth:sanctum + system.config:enable_jobs_module, prefix jobs/stores/{store} - see the caution above: this file is not currently required anywhere, so none of these routes actually resolve.

Method Path Controller method
GET job-types JobTypeController::index
POST job-types JobTypeController::store (store:edit-settings)
PUT job-types/{jobType} JobTypeController::update (store:edit-settings)
DELETE job-types/{jobType} JobTypeController::destroy (store:edit-settings)
GET job-types/{jobType}/properties JobPropertyController::index
POST job-types/{jobType}/properties JobPropertyController::store (store:edit-settings)
PUT properties/{property} JobPropertyController::update (store:edit-settings)
DELETE properties/{property} JobPropertyController::destroy (store:edit-settings)
GET jobs JobController::index
GET jobs/stats JobController::stats
POST jobs JobController::store
GET jobs/{job} JobController::show
PUT jobs/{job} JobController::update
POST jobs/{job}/status JobController::transition
POST jobs/{job}/tasks JobTaskController::store
PUT tasks/{task} JobTaskController::update
DELETE tasks/{task} JobTaskController::destroy
POST jobs/{job}/materials JobMaterialController::store
POST materials/{material}/consume JobMaterialController::consume
POST materials/{material}/return JobMaterialController::returnToStock
POST materials/{material}/write-off JobMaterialController::writeOff
DELETE materials/{material} JobMaterialController::destroy
POST jobs/{job}/labour JobLabourController::store
PUT labour/{labour} JobLabourController::update
DELETE labour/{labour} JobLabourController::destroy
POST jobs/{job}/expenses JobExpenseController::store
DELETE expenses/{expense} JobExpenseController::destroy
POST jobs/{job}/charges JobChargeController::store
PUT charges/{charge} JobChargeController::update
DELETE charges/{charge} JobChargeController::destroy
POST jobs/{job}/attachments JobAttachmentController::store
DELETE attachments/{attachment} JobAttachmentController::destroy
POST jobs/{job}/payments JobPaymentController::store
DELETE payments/{payment} JobPaymentController::destroy (also requires sales:delete)
POST jobs/{job}/invoice JobInvoiceController::store
GET jobs-report/profitability JobController::profitabilityReport
  • Repair - separate RepairJobCard schema, no shared code, but JobInvoicingService deliberately mirrors Repair’s collect/invoice pattern, and ServiceRevenueAnalyticsRepository reports the two side by side
  • HR - JobLabour.employee_id
  • SaleCustomers - Job.customer_id
  • Sales - invoicing a job creates a real Sale via the same SalesService
  • Inventory - JobMaterial/JobCharge reference stock and product variants
  • Pdfs - the job_quotation export kind reads Job directly