Fortnightly Invoice Feature — Implementation Plan
1. Goal
Add a Fortnightly Invoice feature alongside the existing Monthly Invoice. Customers can be set to:
- Monthly (existing,
auto_invoice_type = 2) - Fortnightly (Odd) → processes on odd ISO weeks of the year (e.g., week 1, 3, 5...):
auto_invoice_type = 3 - Fortnightly (Even) → processes on even ISO weeks of the year (e.g., week 2, 4, 6...):
auto_invoice_type = 4
The implementation reuses the existing RunsheetInvoiceCommand, InvoiceManagement repository, and waiting-invoice UI.
2. What's Fast & Already Works
auto_invoice_typeis already a field oncustomerstable and used in blade dropdown and controller validation.waitInvoiceData()already returns eligible order details and is agnostic to invoice frequency.getCombinedWaitInvoiceIds()already handles normal and WIP (overload) payloads.RunsheetInvoiceCommandalready demonstrates the complete automation pattern: query → filter → group by rental vs non-rental → callgenerateInvoiceorgenerateWaitingInvoiceMultiOrder.
So the work is minimal: extend the enum values, add UI options, and teach the cron command about fortnightly weeks.
3. Implementation Changes — Update Summary
3.1 auto_invoice_type Enum Extended
auto_invoice_type values 3 and 4 added to dropdowns across:
resources/views/admin/accounting/customer/edit.blade.phpresources/views/admin/accounting/customer/create.blade.phpresources/views/admin/accounting/customer/show.blade.phpresources/js/components/contracts/ContractCustomerLookup.vue
No migration required; column is existing integer.
3.2 Shared Option Source
Files: app/Helpers/AutoHelper.php, app/Customer.php
AutoHelper::getAutoInvoiceTypeOptions()returns the canonical 5-option list (values 0–4). This single source now drives all Blade dropdowns and API responses.Customer::getAutoInvoiceTypeLabelAttribute()exposes the label for display in Blade ($customer->auto_invoice_type_label).Customer::isBillLater()returnstruefor values 2, 3, and 4, enabling broad monthly/fortnightly parity throughout task and extra-service invoice paths.
3.3 RunsheetInvoiceCommand — Refactored
File: app/Console/Commands/RunsheetInvoiceCommand.php
- Signature updated with
--type=monthly|fortnightly|all, gating run eligibility per type. - Monthly flow preserved; precedence fix:
invoice_monthly_create == 0(end-of-month) takes priority over--dayflag. - Fortnightly flow runs on Mondays only, resolves odd/even via ISO week number (
format('W') % 2), then runs the type that matches today's week bucket. processInvoicesForType(string $type)holds the shared generation body:- Normal IDs:
waitInvoiceData()+whereDoesntHave('invoices')+ customer filter - WIP IDs: overload branch with
skip bindate split (pickup vs. delivery) + unbilled-extrawhereNotExistsguard - Rental grouped by
order_id→generateInvoice() - Non-rental grouped by
customer_id→generateWaitingInvoiceMultiOrder()
- Normal IDs:
getWaitingIds()builds the customer filter inline:type === 'all'→whereIn('auto_invoice_type', [2,3,4])- otherwise maps to single value per type
3.4 Controller & Repository — auto_invoice_type Filter
Files: app/Http/Controllers/Admin/Accounting/InvoiceController.php, app/Repositories/InvoiceManagement.php
New when clause added to the wait-invoice list query, scoping both normal and WIP queries to the selected auto_invoice_type when provided.
3.5 Removed Hardcoded == 2 Checks
Updated across task and extra-service invoice paths in:
app/Repositories/TaskRepository.phpapp/Repositories/TaskExtraServiceRepository.phpapp/Http/Controllers/Admin/Accounting/EwalletController.phpapp/Exports/RunsheetRentalExport.phpapp/Http/Controllers/Admin/FrontBins/FrontBinController.phpapp/Http/Controllers/Admin/RentalBins/RentalBinsController.phpapp/Http/Controllers/Admin/TrashBags/TrashBagController.phpapp/Http/Controllers/Admin/WheelBins/WheelBinController.phpapp/Http/Controllers/Admin/OrderController.phpapp/Http/Controllers/Admin/OrderSkipBinController.phpapp/Http/Controllers/ApiApp/V2/OrderController.phpapp/Http/Resources/ApiApp/V2/CustomerResource.php— passesgetAutoInvoiceTypeOptions()to the API response
All occurrences of auto_invoice_type == 2 in these paths were replaced with $customer->isBillLater() for monthly/fortnightly parity.
3.6 Frontend — Label & Global Helper
Files: resources/js/app.js, resources/js/components/order-management/OrderCustomerInformation.vue
Vue.prototype.$isCustomerBillLateradded as a shared global helper.OrderCustomerInformation.vueuses a localautoInvoiceLabelarray for the email-conflict SweetAlert (intentional, per design review).
4. Data Flow Diagram
5. Files to Touch
| File | Change | Complexity |
|---|---|---|
app/Console/Commands/RunsheetInvoiceCommand.php | Refactored: --type option, processInvoicesForType(), getWeekBucket(), resolveDateRange(), monthly precedence fix | Medium |
app/Helpers/AutoHelper.php | Added getAutoInvoiceTypeOptions() | Low |
app/Customer.php | Added isBillLater() and auto_invoice_type_label accessor | Low |
app/Repositories/InvoiceManagement.php | auto_invoice_type filter in wait-invoice query | Low |
app/Http/Controllers/Admin/Accounting/InvoiceController.php | Request param binding for wait-invoice filter | Low |
app/Repositories/TaskRepository.php | Replaced hardcoded == 2 with isBillLater() | Low |
app/Repositories/TaskExtraServiceRepository.php | Replaced hardcoded == 2 with isBillLater() | Low |
app/Http/Resources/ApiApp/V2/CustomerResource.php | Expose new options in API response | Low |
resources/js/app.js | Added Vue.prototype.$isCustomerBillLater | Low |
resources/js/components/order-management/OrderCustomerInformation.vue | Local label array (intentional) | Trivial |
resources/js/components/contracts/ContractCustomerLookup.vue | Dropdown driven by autoInvoiceTypeOptions prop | Low |
resources/views/admin/accounting/customer/edit.blade.php | Added options 3/4 | Low |
resources/views/admin/accounting/customer/create.blade.php | Added options 3/4 | Low |
resources/views/admin/accounting/customer/show.blade.php | Replaced hardcoded label with $customer->auto_invoice_type_label | Low |
resources/views/admin/accounting/invoice/wait-invoice.blade.php | Added auto_invoice_type filter dropdown | Low |
Multiple controllers and order-type views (EwalletController, FrontBinController, RentalBinsController, TrashBagController, WheelBinController, OrderController, OrderSkipBinController, ApiApp/V2/OrderController) | Replaced hardcoded == 2 with isBillLater() | Low |
app/Exports/RunsheetRentalExport.php | Replaced hardcoded == 2 with isBillLater() | Low |
resources/views/admin/order/show/customer-detail.blade.php | Used isBillLater() + label accessor | Low |
6. Testing Checklist
- Run
php artisan runsheet:invoice --type=monthlyon a non-monthly-run day → should exit gracefully. - Run
php artisan runsheet:invoice --type=monthlyon the configured monthly day → invoices created forauto_invoice_type = 2only. - Run
php artisan runsheet:invoice --type=fortnightly --date=YYYY-MM-DDon an odd-week date → invoices created forauto_invoice_type = 3only. - Run
php artisan runsheet:invoice --type=fortnightly --date=YYYY-MM-DDon an even-week date → invoices created forauto_invoice_type = 4only. - Run
php artisan runsheet:invoice --type=allon any day → only eligible types for today execute. - Customer edit/create/show pages show "Fortnightly (Odd)" and "Fortnightly (Even)".
- Save customer with
auto_invoice_type = 3; confirm it is persisted. - Verify no duplicate invoices are created by running the same command twice on the same day.
- Confirm WIP (overload) invoices are still created correctly for fortnightly customers.
- Verify
/wait-invoicespage shows fortnightly customers' order details and manual generation works. - On
/wait-invoices, "All" filter shows every eligible row regardless ofauto_invoice_type. - Selecting
Monthlyin the new filter shows only customers withauto_invoice_type = 2. - Selecting
Fortnightly (Odd)shows onlyauto_invoice_type = 3. - Selecting
Fortnightly (Even)shows onlyauto_invoice_type = 4. - Combined filters (date + customer + category + invoice type) work together.
- Export (PDF/Excel) respects the selected
invoice_typefilter. - API response
customer.generate_invoicereturns new options. -
Bill Triggerpage dropdown labels include fortnightly options. - All order-type pages that previously checked
auto_invoice_type == 2now also activate for 3/4.
7. Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Old cron only runs monthly and fortnightly customers are never picked up. | Add the new Monday cron with --type=fortnightly. Old daily runsheet:invoice line continues to work for monthly without changes. |
| Duplicate invoices if the command runs multiple times on the same day. | whereDoesntHave('invoices') and WIP unbilled-extra check already prevent duplicates. |
| Week 53 (ISO week number, occurring in some years) | Natural modulo arithmetic handles it: 53 % 2 == 1 (odd), so it maps correctly to the odd-week bucket. |
Manual generation from /wait-invoices bypasses the fortnightly schedule for a customer with auto_invoice_type = 3/4. | Intentional — manual override is an admin feature. The cron handles automated runs only. |
| Partner-level scheduling for fortnightly not yet configurable. | At launch, fortnightly runs daily and self-gates by week parity. If partner control is needed later, add a invoice_fortnightly_day setting. |
Frontend pages that missed the isBillLater() update still behave as monthly-only. | Audit all order-type controllers and views; the shared isBillLater() method makes the change trivial in any remaining locations. |
8. Cron Setup
Add the following entry to the server's crontab (e.g., /etc/cron.d/laravel or the project's app/Console/Kernel.php schedule) to run the fortnightly invoice command every Monday morning:
# Run fortnightly invoice generation every Monday at 2:00 AM
30 1 * * 1 cd /path/to/project && php artisan runsheet:invoice --type=fortnightly >> storage/logs/fortnightly-invoice.log 2>&1
Important notes:
- Day-of-week:
1= Monday (cron syntax0 2 * * 1). The command gates itself internally — it resolves odd/even ISO week parity on the day it runs, so only one of the two fortnightly customer groups is invoiced each Monday. - Time:
2:00 AMis recommended to avoid conflicting with peak server load and to give the monthly command (if also scheduled early morning) a clear window. - Logging: The
>> storage/logs/fortnightly-invoice.log 2>&1redirect captures stdout and stderr for debugging. Ensure thestorage/logsdirectory is writable by the web server user. - Existing monthly cron should remain untouched. If you already have a daily
runsheet:invoice(without--type) orrunsheet:invoice --type=monthlyentry, keep it; the two commands run independently. - Dry-run first: Before activating the cron, test manually with:
Replace the date with any Monday in an odd or even ISO week to verify the correct customer group is invoiced.php artisan runsheet:invoice --type=fortnightly --date=2025-01-06