Skip to main content

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_type is already a field on customers table 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.
  • RunsheetInvoiceCommand already demonstrates the complete automation pattern: query → filter → group by rental vs non-rental → call generateInvoice or generateWaitingInvoiceMultiOrder.

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.php
  • resources/views/admin/accounting/customer/create.blade.php
  • resources/views/admin/accounting/customer/show.blade.php
  • resources/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() returns true for 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 --day flag.
  • 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 bin date split (pickup vs. delivery) + unbilled-extra whereNotExists guard
    • Rental grouped by order_idgenerateInvoice()
    • Non-rental grouped by customer_idgenerateWaitingInvoiceMultiOrder()
  • 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.php
  • app/Repositories/TaskExtraServiceRepository.php
  • app/Http/Controllers/Admin/Accounting/EwalletController.php
  • app/Exports/RunsheetRentalExport.php
  • app/Http/Controllers/Admin/FrontBins/FrontBinController.php
  • app/Http/Controllers/Admin/RentalBins/RentalBinsController.php
  • app/Http/Controllers/Admin/TrashBags/TrashBagController.php
  • app/Http/Controllers/Admin/WheelBins/WheelBinController.php
  • app/Http/Controllers/Admin/OrderController.php
  • app/Http/Controllers/Admin/OrderSkipBinController.php
  • app/Http/Controllers/ApiApp/V2/OrderController.php
  • app/Http/Resources/ApiApp/V2/CustomerResource.php — passes getAutoInvoiceTypeOptions() 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.$isCustomerBillLater added as a shared global helper.
  • OrderCustomerInformation.vue uses a local autoInvoiceLabel array for the email-conflict SweetAlert (intentional, per design review).

4. Data Flow Diagram


5. Files to Touch

FileChangeComplexity
app/Console/Commands/RunsheetInvoiceCommand.phpRefactored: --type option, processInvoicesForType(), getWeekBucket(), resolveDateRange(), monthly precedence fixMedium
app/Helpers/AutoHelper.phpAdded getAutoInvoiceTypeOptions()Low
app/Customer.phpAdded isBillLater() and auto_invoice_type_label accessorLow
app/Repositories/InvoiceManagement.phpauto_invoice_type filter in wait-invoice queryLow
app/Http/Controllers/Admin/Accounting/InvoiceController.phpRequest param binding for wait-invoice filterLow
app/Repositories/TaskRepository.phpReplaced hardcoded == 2 with isBillLater()Low
app/Repositories/TaskExtraServiceRepository.phpReplaced hardcoded == 2 with isBillLater()Low
app/Http/Resources/ApiApp/V2/CustomerResource.phpExpose new options in API responseLow
resources/js/app.jsAdded Vue.prototype.$isCustomerBillLaterLow
resources/js/components/order-management/OrderCustomerInformation.vueLocal label array (intentional)Trivial
resources/js/components/contracts/ContractCustomerLookup.vueDropdown driven by autoInvoiceTypeOptions propLow
resources/views/admin/accounting/customer/edit.blade.phpAdded options 3/4Low
resources/views/admin/accounting/customer/create.blade.phpAdded options 3/4Low
resources/views/admin/accounting/customer/show.blade.phpReplaced hardcoded label with $customer->auto_invoice_type_labelLow
resources/views/admin/accounting/invoice/wait-invoice.blade.phpAdded auto_invoice_type filter dropdownLow
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.phpReplaced hardcoded == 2 with isBillLater()Low
resources/views/admin/order/show/customer-detail.blade.phpUsed isBillLater() + label accessorLow

6. Testing Checklist

  • Run php artisan runsheet:invoice --type=monthly on a non-monthly-run day → should exit gracefully.
  • Run php artisan runsheet:invoice --type=monthly on the configured monthly day → invoices created for auto_invoice_type = 2 only.
  • Run php artisan runsheet:invoice --type=fortnightly --date=YYYY-MM-DD on an odd-week date → invoices created for auto_invoice_type = 3 only.
  • Run php artisan runsheet:invoice --type=fortnightly --date=YYYY-MM-DD on an even-week date → invoices created for auto_invoice_type = 4 only.
  • Run php artisan runsheet:invoice --type=all on 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-invoices page shows fortnightly customers' order details and manual generation works.
  • On /wait-invoices, "All" filter shows every eligible row regardless of auto_invoice_type.
  • Selecting Monthly in the new filter shows only customers with auto_invoice_type = 2.
  • Selecting Fortnightly (Odd) shows only auto_invoice_type = 3.
  • Selecting Fortnightly (Even) shows only auto_invoice_type = 4.
  • Combined filters (date + customer + category + invoice type) work together.
  • Export (PDF/Excel) respects the selected invoice_type filter.
  • API response customer.generate_invoice returns new options.
  • Bill Trigger page dropdown labels include fortnightly options.
  • All order-type pages that previously checked auto_invoice_type == 2 now also activate for 3/4.

7. Risks & Mitigations

RiskMitigation
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 syntax 0 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 AM is 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>&1 redirect captures stdout and stderr for debugging. Ensure the storage/logs directory is writable by the web server user.
  • Existing monthly cron should remain untouched. If you already have a daily runsheet:invoice (without --type) or runsheet:invoice --type=monthly entry, keep it; the two commands run independently.
  • Dry-run first: Before activating the cron, test manually with:
    php artisan runsheet:invoice --type=fortnightly --date=2025-01-06
    Replace the date with any Monday in an odd or even ISO week to verify the correct customer group is invoiced.