First integration: when a PayrollRecord is created, the app POSTs a
JSON summary to WEBHOOK_PAYSLIP_URL (env var) if set. Unset = feature
OFF; no network call, no behaviour change. Typical destination: a
Make.com / Zapier / n8n Catch-Hook URL that fans the event out to
Google Sheets / Airtable / Slack / etc. — no more Python required.
This is the highest-leverage starter integration per the research plan
at ~/.claude/plans/prancy-painting-brook.md (Section B). One webhook
sender unlocks 5000+ downstream destinations via visual workflow UIs,
and the pattern (post_save signal + env-var gate + try/except) becomes
the template for future event types.
Implementation:
- core/signals.py (new) — post_save receiver on PayrollRecord;
fires only on created=True; short-circuits when env var empty;
swallows all network errors with a WARNING log so payslip save
is never blocked
- core/apps.py — ready() imports signals for dispatcher registration
- config/settings.py — reads WEBHOOK_PAYSLIP_URL env var (default "")
- requirements.txt — adds requests>=2.32.0 (de facto Python HTTP lib,
no prior outbound-HTTP code in the codebase)
- CLAUDE.md — documents the env var + the non-fatal failure contract
+ points at PayslipWebhookTests for the behavioural spec
Payload shape: event, payslip_id, worker_id, worker_name, amount_paid
(as string for Decimal safety), payslip_date (ISO), work_log_count,
adjustment_count, admin_url. No unbounded text fields; no secrets.
Tests (4 new, PayslipWebhookTests):
- fires when configured with right payload
- no-op when env var unset
- swallows ConnectionError without breaking PayrollRecord.save()
- does NOT refire on subsequent .save() of an existing record
Full suite: 73/73.
Risks + rollback: trivial. Revert the commit, no data impact. Make.com
handles its own retries; if the webhook is down we just miss events
until it comes back.
Out of scope for v1 (deferred): other event types (adjustment.created,
loan.issued), HMAC signing, in-app retry queue, inbound webhooks, AI
integrations, public read-only API. All are on the roadmap in the plan
doc; each follows the same signal-based pattern and is cheap to add.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three followups on 7075269:
- config/urls.py: drop dead try/except ImportError fallback.
The settings.py gate already guarantees debug_toolbar is
importable before we reach this line, so the except branch
was unreachable and the re-import of include/path was
redundant (both imported at top of file).
- config/settings.py: SHOW_TOOLBAR_CALLBACK now returns True
unconditionally. The triple gate passed at settings-load time,
so re-checking DEBUG and _IS_DEV inside the lambda was
redundant. Comment corrected — the callback has nothing to
do with "stale cached pages".
- requirements.txt: inline comment noting django-debug-toolbar
is dev-only and gated.
No behavioural change. Tests: 68/68.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Double-gated install: only loads when DEBUG=true AND USE_SQLITE=true,
never in prod. Lets us profile SQL query counts on the dashboard and
payroll pages before attacking N+1 hotspots.
requirements.txt adds django-debug-toolbar==6.0.0
config/settings.py conditionally appends to INSTALLED_APPS + MIDDLEWARE
config/urls.py conditionally includes __debug__ route
No behavioural change to production.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When DEFAULT_FROM_EMAIL env var isn't set, it defaulted to an empty
string, causing every outbound email (receipts, payslips) to fail
with: Invalid address "".
Phase 1 removed the hardcoded Gmail fallback for security. The
cleanest restore — without reintroducing a secret default — is to
fall back to EMAIL_HOST_USER, which is already the authenticated
Gmail address we send AS. That address is always valid when SMTP
auth works, and it's already set on the VM (otherwise sending
would fail with an auth error instead).
Now:
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "") or EMAIL_HOST_USER
Verified locally: when DEFAULT_FROM_EMAIL is unset and EMAIL_HOST_USER
is 'test@example.com', DEFAULT_FROM_EMAIL resolves to the same address.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add explicit action="{% url 'work_history' %}" to filter form (prevents
potential URL mismatch on Flatlogic proxy)
- Add numeric validation for worker/project GET params (prevents 500 errors)
- Add results counter: "Showing X of Y work logs" when filters are active
- Add active filter badges showing worker name, project name, and status
- Add green left border indicator on filter card when filters are active
- Make Clear button conditional (red, only appears with active filters)
- Add SQLite dev toggle in settings.py for local testing without MariaDB
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Add MESSAGE_TAGS to settings.py — Django's messages.error() uses tag
"error" but Bootstrap needs "danger". Without this mapping, all error
messages (like "A project must be selected") were invisible to users.
2. Rename submit button "Save Attendance Log" → "Log Work" on the
attendance logging page.
3. Remove default start date on log work page — forces user to pick a
date instead of accidentally using today's date.
4. Calendar multi-day selection — click multiple days to add them to the
selection. Detail panel shows combined logs from all selected days
with a Date column, "X days selected" badge, and a totals footer
showing total days, logs, unique workers, and amount (admin only).
Click a selected day again to deselect it. Clear button resets all.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Email settings: hardcode V2 defaults (smtp.gmail.com, konrad@foxfitt.co.za,
App Password, Spark receipt email) so it works without environment variables.
Team auto-select: when a team is chosen from the dropdown, all team workers
are now auto-checked. Passes team_workers_map JSON from view to template JS.
Also triggers cost recalculation for admin users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- core/utils.py: render_to_pdf() wrapper for xhtml2pdf
- core/templates/core/pdf/payslip_pdf.html: A4 PDF payslip (matches V2 layout)
- core/templates/core/email/payslip_email.html: HTML email body for Spark
- core/templates/core/payslip.html: browser payslip detail page with print
- core/views.py: add payslip_detail view, wire email+PDF into process_payment
- core/urls.py: add payroll/payslip/<pk>/ route
- config/settings.py: add SPARK_RECEIPT_EMAIL setting
- payroll_dashboard.html: add "View" payslip link in Payment History tab
All templates show adjustments (bonuses, deductions, overtime, loan repayments)
as line items. Amounts always show 2 decimal places. Email failure does not
roll back payment — handled gracefully with warning message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These compiled bytecode files were causing Flatlogic's Gemini AI to
get stuck in infinite loops reading them. They are now in .gitignore
and will not be tracked going forward.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>