Commit Graph

1343 Commits

Author SHA1 Message Date
gsinghpal
e596723ba5 fix(fusion_helpdesk): render message bodies as HTML, not escaped text
The OWL dialog used <t t-out="m.body"/> on message bodies, but t-out
escapes plain strings — it only renders raw when the value is a Markup
instance. Bodies arrive over JSON-RPC as plain strings (Markup is a
client-side type, doesn't cross the wire), so the customer was seeing
literal "<p>This has been fixed.</p>" in the thread instead of the
rendered HTML.

Wrap incoming bodies in `markup()` at the boundary (openTicket +
sendReply call sites) so the template renders them as the sanitised
HTML the central chatter already produced. Trust is fine — the body is
sanitised server-side by mail.thread before it ever leaves nexa.

Bumps fusion_helpdesk to 19.0.1.7.1.
2026-05-27 11:40:17 -04:00
gsinghpal
d7ec91b0f1 feat(fusion_helpdesk): Critical flag, KPI cards, colored stage pills
Three coordinated changes on top of the section grouping:

1. **Mark as Critical** — a red chip on the New tab sets priority='3'
   when submitted. The central post-create hook auto-applies a "Critical"
   helpdesk.tag (shipped via fusion_helpdesk_central data XML, noupdate=1
   so support can recolor without losing it on upgrade), giving support
   a kanban-groupable signal that doesn't rely on remembering what
   priority='3' means. Scoped to in-app-channel tickets only, so a
   support agent manually setting Urgent on their own ticket isn't
   silently tagged.

2. **KPI cards above the sections** — Total / Open / Closed / Critical
   in a 4-up grid (auto-collapses to 2x2 under 540px). Each card uses
   its own saturated gradient so it reads on both light and dark mode —
   the dialog backdrop is irrelevant because the gradient brings its
   own background. Counts are computed in JS from state.tickets so they
   always match what's rendered below.

3. **Colored stage pills** — red Critical, green Solved, dark-yellow New,
   orange Cancelled, blue for In Progress / Testing / On Hold. Critical
   priority gets a *separate* red pill alongside the stage pill so you
   keep stage info even on escalated tickets. Stage matching is
   substring-based (lowercased) so a renamed "Resolved" or "Done" stage
   on central still maps to the green pill.

Tests cover the new is_critical=True → priority='3' wiring and the
default omission so SLA / stage defaults keep working for normal
tickets. Bumps fusion_helpdesk to 19.0.1.7.0 and
fusion_helpdesk_central to 19.0.1.2.0. End-to-end smoke test verified
live: priority=3 + x_fc_client_label triggers the Critical tag.
2026-05-27 11:21:11 -04:00
gsinghpal
3e5ced1655 feat(fusion_helpdesk): group My Tickets into Critical/New/Solved sections
The flat write_date-sorted list was hard to scan with 50+ tickets — solved
ones were intermixed with active ones, and there was no signal for
priority. Bucket each ticket server-side into 'critical' (open + priority
High/Urgent), 'solved' (stage marked fold=True on central) or 'open'
(everything else), and render three labelled sections in the dialog with
sticky headers, count badges, and per-group accent colours. Backend keeps
its write_date desc order so latest is always at top within each bucket.

Bucketing uses helpdesk.stage.fold (not the stage name) so renaming
"Solved" to "Done" on the central won't quietly mis-categorise rows.
Adds bucket_ticket() in utils.py with unit tests covering the
folded-wins-over-priority precedence and the missing-priority fallback.

Also surfaces a small Urgent (triangle) / High (arrow) icon on each row
so a critical ticket reads at a glance even after a user scrolls past
the section header.

Bumps fusion_helpdesk to 19.0.1.6.0.
2026-05-27 11:04:31 -04:00
gsinghpal
aabfc1afe7 fix(fusion_helpdesk): auto-grant reporter admin to system admins + doc backfill
The customer-followup ship left two papercuts that hid 51 historical
tickets from the entech owner:

1. group_reporter_admin had zero members on install — the new XML record
   created the group but never granted it. Extend base.group_system's
   implied_ids so every system administrator transparently inherits the
   admin view of the embedded inbox on install / upgrade. (4, id) tuple
   is additive — never replaces base's existing implications.

2. Tickets created before this feature shipped had NULL
   x_fc_client_label and NULL partner_email, so the scope filter
   excluded them all. The reporter identity was still recoverable from
   the description HTML's diag block. Backfill recipe is captured in
   CLAUDE.md so future deployments can apply the same one-shot UPDATE
   without re-deriving the regex.

Bumps fusion_helpdesk to 19.0.1.5.0. Verified live on entech: all six
base.group_system members now return True for
has_group('fusion_helpdesk.group_reporter_admin').
2026-05-27 10:54:51 -04:00
gsinghpal
45b698beb5 feat(configurator): per-customer default lead time on partner profile
Adds two Integer fields to res.partner:
  - x_fc_default_lead_time_min_days
  - x_fc_default_lead_time_max_days

Set once on the customer's Plating Defaults tab (Fulfilment group);
auto-copies onto every new Express Order via the existing
_onchange_partner_id hook. Operator can still override per-order
since the onchange only fills when the wizard field is still blank.

Field declaration lives in fusion_plating_configurator (alongside
the rest of the partner cascade reads). View edit lives in
fusion_plating_invoicing where the Plating Defaults tab already
hosts the other partner-level defaults (invoice strategy, deposit
%, delivery method, deadline-days). Invoicing depends on
configurator, so the fields are registered before the view loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:38:36 -04:00
gsinghpal
de6336ba42 changes 2026-05-27 10:36:48 -04:00
gsinghpal
c876767755 Merge branch 'main' of https://github.com/gsinghpal/Odoo-Modules 2026-05-27 10:36:37 -04:00
gsinghpal
d1fc3d8720 fix(express): show Tax on totals + add tooling as real SO line
Three related fixes on the Express Orders totals card:

1. Totals card now breaks out Subtotal / Tax / Tooling Charge /
   Grand Total. Previously the "Subtotal" and "Grand Total" rows
   both read from total_amount (same value rendered twice) and no
   tax was shown at all. Customers on a fiscal position-mapped
   tax rate (Ontario HST, etc.) had their taxes silently dropped
   from the preview.

2. tooling_charge now feeds the Grand Total. The total_amount
   compute previously summed line subtotals only. Added a real
   SO line for the tooling charge in action_create_order so the
   eventual sale.order.amount_total matches the preview AND the
   invoice carries a "Tooling Charge" line item.

3. tax_ids is now visible as an optional column on the lines
   list. Operator can see + override the auto-applied tax per
   line. Default still comes from FP-SERVICE product mapped
   through partner.property_account_position_id (fiscal position).

New compute fields on fp.direct.order.wizard:
  - total_subtotal (sum of line.qty * line.unit_price, pre-tax)
  - total_tax (sum of line + tooling taxes via compute_all)
  - total_amount (subtotal + tax + tooling — was just subtotal)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:33:31 -04:00
gsinghpal
a78ceaba51 docs(claude): fusion_helpdesk deploy procedures + 2026-05-27 handoff
Durable: nexa/entech upgrade commands, central service-account Contact
Creation prerequisite, backup-outside-addons-path gotcha, smoke-tests-must-
call-the-controller lesson. Plus current deploy status + the one remaining
step (browser confirmation of My Tickets / New on entech).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:26:21 -04:00
gsinghpal
6c15a7b1cf feat(fusion_helpdesk): customer follow-up + embedded ticket inbox
Squash-merge of feat/helpdesk-customer-followup. The billing and
fusion_login_audit work from that branch is already on main (landed
separately); this lands only the helpdesk feature.

- Identity keystone: submit() forwards partner_email/partner_name/
  x_fc_client_label so the central Helpdesk find-or-creates the customer
  partner and subscribes them as a follower (enables reply emails + magic link).
- Embedded in-app 'My Tickets' inbox: server-side scoped read/reply RPC
  endpoints, per-user seen tracking (fusion.helpdesk.ticket.seen), systray
  unread badge. Defense-in-depth scope domain + _norm_email normalisation
  (wildcard emails cannot widen scope).
- fusion_helpdesk_central: x_fc_client_label field + list/search views +
  branded acknowledgement email template.
- Deployed and smoke-tested live: nexa central 19.0.1.1.0, entech client
  19.0.1.4.1 (requires Contact Creation on the central service account).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:23:33 -04:00
gsinghpal
45ddb444a7 docs(billing): handoff — note fusion_login_audit also landed on main 2026-05-27 09:06:16 -04:00
gsinghpal
9df3262d30 fix(fusion_login_audit): avoid duplicate row on bad-password
When the login string resolves to an existing user and the password is
wrong, BOTH overrides used to write a failure row:
  - _check_credentials wrapper: result=failure, reason=bad_password
  - _login wrapper (catching the propagating AccessDenied): result=
    failure, reason=unknown_user

Discovered in production smoke on westin-v19 after the deploy: a
single failed login for info@gsafinancialconsulting.com produced two
audit rows (one bad_password, one unknown_user). The unknown_user
label was wrong — the user IS in the system.

Fix: _login now checks whether the login string resolves to any user
BEFORE writing the unknown_user row. If yes, _check_credentials
already logged the attempt and _login skips. If no, the user lookup
in super() failed and _login is the only chance to log.

Regression test test_login_known_user_bad_password_single_row asserts
exactly one row per attempt and that the row carries bad_password
(not unknown_user) when the user exists.

30 tests green locally; production smoke on westin-v19 confirms:
one row per failed login, bad_password, IP 172.18.0.1 captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
5d9609b5ee chore(fusion_login_audit): release 19.0.1.0.0
Module is feature-complete per
docs/superpowers/specs/2026-05-26-fusion-login-audit-design.md:
- T1  Module skeleton + icon
- T2  fusion.login.audit model (16 fields, declarative Constraint+3 Indexes)
- T3  Security: ACL + admin-only record rule + 5 tests
- T4  _fc_build_event_vals context helper (UA parse, password safety)
- T5  Success hook: _update_last_login -> result=success row
- T6  Bad-password hook: _check_credentials wrapped
- T7  Unknown-user hook: _login override (instance method in 19)
- T8  res.users smart button + Login Activity tab (4 x_fc_* fields)
- T9  Standalone list/form/search/kanban + 2 actions + 3 menus
- T10 res.config.settings + General Settings section (4 knobs)
- T11 Failure-burst alert email + 60-min cooldown
- T12 Daily retention GC cron
- T13 5-min async geo enrichment cron (private/cache/HTTP)
- T14 View-visibility security tests for non-admin
- 29 tests pass; both crons active; 3 menus installed.

Out of scope for v1 (documented in spec): API-key auth, OAuth/SSO,
per-user self-service view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
622f133f05 docs(plan): inline corrections from T11/T12/T13 execution
Capture in the plan the Odoo 19 gotchas discovered during execution
that the original plan template missed:
- Test command requires --http-port=0 --gevent-port=0 (running
  container holds 8069).
- Declarative models.Constraint / models.Index (T2).
- res.users.groups_id renamed to group_ids (T3, T6).
- ir.rule groups is additive not restrictive (T3).
- mail.template inline-template ctx IS env.context (T11).
- ir.cron has no numbercall field in 19 (T12).
- registry.cursor() in tests is TestCursor; cr.commit() raises;
  use savepoints (T13).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
482f12256e test(fusion_login_audit): view-visibility checks for admin vs non-admin
Asserts the smart-button and Login Activity tab fields are stripped
from res.users get_view() for non-admin users, and present for
Settings admins. Locks down the contract behind the
groups="base.group_system" XML attributes on the form-inheritance
view (the inherited view record cannot carry groups itself per
CLAUDE.md rule #11; the gate must live on the inner nodes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
86b8e59c95 feat(fusion_login_audit): async geo enrichment cron
5-min cron processes up to 100 pending rows per pass: private IPs
short-circuit to state=private_ip; same-IP cache (30 days) avoids
duplicate ip-api.com calls; reverse DNS via socket with 1.5s timeout;
HTTP lookup respects ip-api''s X-Rl rate-limit header. Tests cover
private-IP shortcut, cache hit (no HTTP), and internal-state skip --
no network calls needed.

Per-row isolation uses cr.savepoint() instead of cr.commit() because
Odoo 19 TestCursor raises AssertionError on commit/rollback. Recorded
the gotcha as CLAUDE.md rule #14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
1b8038d8e8 feat(fusion_login_audit): nightly retention GC cron
Adds _fc_retention_gc() that deletes rows older than the configured
horizon (default 365 days; 0 = keep forever). Registered as a daily
ir.cron. Tests verify both the delete path and the "keep forever"
short-circuit.

Also documents the Odoo 19 gotcha that ir.cron dropped the numbercall
field (the legacy "-1 = run forever" pattern now raises ValueError at
install time; just omit the field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
a2d13cf83b feat(fusion_login_audit): failure-burst alert email + cooldown
Mail template + helpers (_fc_alert_*, _fc_recent_failure_count,
_fc_send_failure_alert) wired into _check_credentials so that crossing
the consecutive-failure threshold within the window queues exactly one
mail.mail per attempted login per 60-minute cooldown. Master switch
x_fc_login_audit_alert_enabled honoured. Recipients are members of
base.group_system with a non-empty email and share=False; the
__system__ superuser is excluded by Odoo''s default user filter.

Tests (3 new, 22 total green):
  test_failure_burst_queues_one_email
  test_cooldown_suppresses_second_alert
  test_alert_disabled_master_switch

setUp ensures base.user_admin has an email (fusion-dev''s admin user
ships without one; the only user with an email is __system__, which
is filtered out of standard res.users searches).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
6f6aa6e90a feat(fusion_login_audit): settings model + page section
Four x_fc_* fields on res.config.settings backed by ir.config_parameter:
retention_days (default 365, 0 = forever), alert_threshold (5),
alert_window_min (15), alert_enabled (True). New "Login Audit" block
on the General Settings page (gated by base.group_system on the block,
NOT on the inherited view record per CLAUDE.md rule #11).

CLAUDE.md gotchas added during this task:
  #5 Boolean config_parameter fields don't round-trip "False" as a
     string — IrConfigParameter.set_param deletes the row on falsy.
     Test with assertFalse, never assertEqual(..., "False").
  #6 ir.ui.view uses group_ids (Odoo 19 rename mirrored from res.users).
     Setting groups_id on an ir.ui.view record raises ValueError at
     install. (The XML attribute groups="..." on inner nodes is
     unrelated and still works.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
0513ea23a4 feat(fusion_login_audit): standalone views + menus
List, form, and search views for fusion.login.audit, plus a "Login
Events" full-history action and a "Failed Logins (24h)" pre-filtered
action. Both surface under Settings -> Technical -> Login Audit
(menu items gated by base.group_system). Views are no-create / no-edit
/ no-delete to enforce append-only at the UI layer too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:59 -04:00
gsinghpal
72aa28e6c4 feat(fusion_login_audit): smart button + Login Activity tab on res.users
Adds four x_fc_* fields on res.users: login_audit_ids (One2many),
login_audit_count (compute), last_successful_login (compute, stored),
last_login_ip (compute, stored). action_fc_view_login_audit returns
a window action scoped to the current user. View inheritance adds a
smart button to the button box and a "Login Activity" page to the
notebook, both gated by base.group_system on the inner XML nodes
(NOT on the view record — Odoo 19 forbids that; see CLAUDE.md rule #11).

Tests (2 new, 18 total green):
  test_computed_last_successful_login — uses registry cursor to commit
    the audit row so the stored compute picks it up across the
    TransactionCase boundary.
  test_action_view_login_audit_returns_window_action — smart-button
    action shape + domain scoping.

CLAUDE.md rule #11 added: inherited ir.ui.view records cannot have
groups/group_ids on the record; the gate must be on the inner XML nodes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
a7cf44249d feat(fusion_login_audit): hook unknown-user failures via _login
Overrides res.users._login. When the login string does not resolve to
any user, super() raises AccessDenied; we record a row with user_id=NULL
and failure_reason="unknown_user", then re-raise. Closes the gap where
typo'd or scanned logins would otherwise vanish from the audit trail.

The existing _fc_record_login_event helper writes through an independent
registry.cursor(), so the audit row survives the rollback that follows
the re-raised AccessDenied.

Note: in Odoo 19 _login is a plain instance method (not the classmethod
it was in earlier versions) and takes (credential, user_agent_env). The
original plan was written for the classmethod signature; corrected here
and recorded in CLAUDE.md rule #10 so future-Claude does not waste time
re-discovering it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
0e6ebe7bc6 feat(fusion_login_audit): hook bad-password failures via _check_credentials
Wraps res.users._check_credentials. On AccessDenied, records a row with
result=failure and failure_reason='bad_password' (or '2fa_failed' when
credential['type'] == 'totp'), then re-raises. Regression test asserts
the attempted password value never lands in any audit field.

The audit row is written through registry.cursor() (independent cursor) so
it survives the rollback that follows AccessDenied — in production
odoo/service/model.py::retrying resets the transaction and http.py closes
the cursor without committing, in tests assertRaises opens its own
savepoint. Either way an inline write would vanish. Tests
enter registry_test_mode and use manual try/except to keep the audit row
visible across the savepoint hierarchy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
dced0c66a4 feat(fusion_login_audit): hook successful login via _update_last_login
Overrides res.users._update_last_login to create a fusion.login.audit
row with result=success after the parent runs. The write goes through
sudo() + mail_create_nolog=True. Any exception in the audit path is
caught and logged but never propagates — a broken audit table must
never block a real user from logging in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
2ced576204 feat(fusion_login_audit): add _fc_build_event_vals context helper
Single helper builds vals for fusion.login.audit rows from the live
HTTP request, or falls back to ip=''internal'' + geo_lookup_state=''internal''
when there is no request. Parses UA into browser/os/device_type via the
bundled user_agents library. Never reads credential[''password'']. Tests
cover: no-request fallback, UA parsing on a Chrome/Windows UA, and the
regression that no password value leaks into the vals dict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
61a0cb244f feat(fusion_login_audit): admin-only record rule + security tests
Record rule grants admins an unrestricted domain on the audit log;
ACL forbids write/create/unlink for every group (audit is append-only;
sudo() inside auth hooks is the only write path). Defence-in-depth
layering: ACL is the actual gate, the rule documents and locks down
admin access path.

Tests (5, all green) cover:
  test_admin_can_read_through_acl_and_rule — positive path through both.
  test_acl_blocks_read_for_regular_user    — base.group_user denied by ACL.
  test_acl_blocks_read_for_portal_user     — base.group_portal share user
                                             denied (sensitive data leakage
                                             surface closed at ACL layer).
  test_acl_blocks_write_for_admin          — append-only at the write boundary.
  test_acl_blocks_unlink_for_admin         — append-only at the unlink boundary.

Drop the redundant `from . import tests` from the root __init__.py —
Odoo's test loader imports `odoo.addons.<mod>.tests` directly; the
extra import was dead weight (and inconsistent with the repo pattern).

CLAUDE.md gotchas added during this task:
  #6 res.users.groups_id -> group_ids rename (test setUp pitfall).
  #6 ir.rule `groups` is additive, not restrictive — group-scoped
     rules only apply to users in that group, they do not restrict
     non-members. Default to letting the ACL gate; use rules for
     row-level filters ACLs cannot express.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
aeea670064 feat(fusion_login_audit): add fusion.login.audit model
- All 16 columns per spec (user, attempted_login, result, failure_reason,
  event_time, ip/geo fields, user_agent triple, device_type, database).
- Check constraint binds failure_reason presence to result value.
- Three composite indexes (user+time, login+time, geo_state+time) supporting
  the per-user, failure-burst, and geo cron queries.
- Minimal admin-read ACL added so subsequent tests can verify writes.
- 3 TransactionCase tests passing: model create, failure_reason nullable on
  success, geo_lookup_state='internal' accepted.

Odoo 19 deprecation note: this implementation uses the declarative
models.Constraint and models.Index attributes (Odoo 19 silently drops the
legacy `_sql_constraints = [...]` list and `init()`/raw-SQL pattern with
only a warning). Captured in CLAUDE.md rule #9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
b0836e1c93 feat(fusion_login_audit): module skeleton + icon
Empty installable module with manifest, package inits, and icon.
Subsequent tasks add the audit model, hooks, views, and tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
a32946be44 docs(plan): fusion_login_audit implementation plan
15 TDD tasks targeting ~28 tests:
T1 skeleton+icon, T2 model+indexes, T3 security, T4 capture helper,
T5 success hook, T6 bad-password hook, T7 unknown-user hook, T8 user
form (smart button + tab + computes), T9 standalone views + menus,
T10 settings + page section, T11 failure-burst alert + cooldown,
T12 retention GC cron, T13 async geo enrichment cron, T14 view
visibility security tests, T15 manual smoke + release tag.

Self-reviewed: every spec section maps to a task; no placeholders;
method and field names consistent across tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
01a85c475c docs(spec): fusion_login_audit design
Durable login audit for Odoo 19 (westin-v19). Captures successful and
failed authentications via _update_last_login / _check_credentials /
_login overrides, surfaces history on res.users as a smart button +
"Login Activity" tab (admins-only), async geo-enriches IPs via ip-api.com
through network_logger, 365-day retention with daily GC cron, and
emails Settings admins on N consecutive failures for the same login
within a configurable window.

Motivation: a spot audit of GSA Accounting (uid 63) showed Odoo's
res_users_log keeps only one row per user (rest is GC'd), /var/log/odoo
is empty (warn-level stdout logging), and the container json log
rotates within days — leaving no durable login trail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:03:58 -04:00
gsinghpal
43b2edcbb5 @
docs(billing): session handoff — core on main, sub-project #2 (NexaCloud) next

Captures resume state for the centralized-billing initiative: core engine done
and on main, the 4-chunk decomposition of sub-project #2 (NexaCloud adapter +
dual-run reconciliation), the pending "where to start" decision, open questions,
and the test/branch workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-27 08:56:28 -04:00
gsinghpal
d770c0c3a9 fix(billing): resolve code-review findings (authz, cross-billing, validation, webhook integrity)
- C1/H4: rating cron only rates subs on the charge's own plan_id
- C1: _fc_rate_usage skips creating a line when amount is 0 (still updates existing)
- C2/C4: /usage authorizes each event (exists + is_subscription + linked customer)
- C3: API handlers validate input and return 4xx-shaped errors instead of raising;
       controller maps status=='error' to HTTP 400
- H1: cron uses real billing window [last_invoice_date or start_date, next_invoice_date)
- H2: _aggregate uses half-open window anchored on period_start
- H3: idempotency scoped to (subscription_id, metric_id, idempotency_key)
- H5: webhook stores canonical body, signs+POSTs it verbatim, adds X-Fusion-Event-Id,
       caps backoff at 2**min(attempts,10)
- H6: SSRF guard rejects non-https / localhost / private / link-local webhook_url
- M7: charge_model reduced to standard/package (dropped unimplemented graduated/volume)
- L1: currency_id required on charge + reconciliation
- L2: charge price non-negative + unit_batch positive DB constraints

Adds 17 regression tests (suite 22 -> 39, all green via fcb_test_on_trial.sh).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
a5db0fe71e feat(billing): usage-rating + webhook-dispatch crons
- SaleOrder._fc_rate_usage: aggregates usage, computes overage via
  charge._compute_billable, upserts sale.order.line for the overage product
- FusionBillingUsage._cron_rate_open_periods: hourly cron iterates active
  charges × in-progress subscriptions, calls _fc_rate_usage
- data/ir_cron.xml: two crons — rate usage (hourly), dispatch webhooks (2 min)
- __manifest__.py: registers data/ir_cron.xml in data list
- test_usage.py: test_rate_open_period_creates_overage_line (TDD, FCB_EXIT=0)

Reference: _create_recurring_invoice / _get_invoiceable_lines confirmed in
Enterprise sale_subscription/models/sale_order.py — overage line goes onto
sale.order so native invoicing picks it up via _get_invoiceable_lines.
2026-05-27 08:42:08 -04:00
gsinghpal
c44fd89ed1 feat(billing): wire HTTP controllers to API handlers 2026-05-27 08:42:08 -04:00
gsinghpal
6c395709cf feat(billing): outbound webhook engine (HMAC + retry/backoff)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
0754d0b101 feat(billing): subscription creation handler (sale.order is_subscription)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
2435096f32 feat(billing): inbound API handlers (customer/usage/catalog)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
25952cf226 feat(billing): period usage aggregation by metric function
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
eb1ee85d24 feat(billing): idempotent usage ingestion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
1e34a67384 feat(billing): metered charge math (quota + overage)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
a1cfab6fe9 feat(billing): identity resolution external account -> partner 2026-05-27 08:42:08 -04:00
gsinghpal
a46e31e710 feat(billing): service API-key generation + matching
Add _match_api_key() class method to fusion.billing.service, with a
TDD test suite (TestServiceApiKey) covering key generation, hash storage,
positive match, and rejection of bad/inactive keys. Also fix
fcb_test_on_trial.sh to use --http-port 8070, as Odoo 19 forces
http_spawn() even under --no-http when --test-enable is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:42:08 -04:00
gsinghpal
032b10752e test(billing): odoo-trial Enterprise test runner + plan test-env fix
Local dev Odoo is Community (can't install the module). Add a guest-exec runner
that syncs the module to the odoo-trial Enterprise sandbox (VM 316, db trial) and
runs --test-enable there; pass = FCB_EXIT=0. Scaffold verified installing on
Odoo 19.0 Enterprise (7 fusion_billing_* tables created).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:40:51 -04:00
gsinghpal
e7d63a3859 docs(billing): core engine implementation plan (TDD, 11 tasks) 2026-05-27 08:40:51 -04:00
gsinghpal
2b47bd8b10 feat(billing): design + scaffold fusion_centralize_billing
Centralize billing for all NexaSystems services (NexaCloud, NexaDesk,
NexaMaps, custom apps, memberships) on the Odoo 19 Enterprise instance,
replacing Lago. The module adds only the metering + integration layer;
native sale_subscription / account_accountant / payment_stripe do all the
financial work (invoicing, HST, dunning, portal, credit notes, Stripe).

Includes:
- Design spec (docs/superpowers/specs/2026-05-27-nexa-billing-centralized-design.md):
  6 locked decisions, architecture, data model, usage engine, Lago-shaped
  API, webhook control loop, NexaCloud pilot, phased dual-run migration.
- Module scaffold: 7 fusion.billing.* models (service, account.link, metric,
  charge, usage, webhook, reconciliation), bearer-auth API controller shell,
  security ACLs, README. Compiles on Odoo 19.0; engine/API bodies are stubs
  pending the implementation plan.
- CLAUDE.md rule #15: no sale.subscription model in Odoo 19 — a subscription
  is a sale.order(is_subscription) + sale.subscription.plan (verified live).

Task 0 verified: a single Stripe account is shared across NexaCloud and all
Lago providers, so no Stripe account/card migration is required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:40:51 -04:00
gsinghpal
2f74d5ecb9 fix(plating): add 3 missing icons to process.node Selection
fp.step.template rows already held 'fa-bathtub' (1), 'fa-flag' (2),
and 'fa-undo' (2) — all plating-relevant and presumably valid in an
earlier version of the Selection list. When step_insert snapshot-
copied these into a fresh fusion.plating.process.node via
_copy_snapshot_fields, the ORM rejected them with
ValueError: Wrong value for fusion.plating.process.node.icon
because they weren't in the curated 39-icon list anymore.

Adding 'fa-bathtub' (bathtub / tank / soak), 'fa-flag' (flag /
milestone / gate), and 'fa-undo' (undo / rework / rerun) to the
process.node Selection. Aligns the two lists (template uses
_get_icon_selection -> node._fields['icon'].selection at runtime).

No data migration needed — existing template rows immediately
re-validate against the wider Selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:43:38 -04:00
gsinghpal
f8abadfc18 fix(configurator): OPEN button errored on missing action.views
FpExpressActionBtns.onOpen called action_open_part which returned an
ir.actions.act_window dict without a 'views' key. Odoo 19's
_preprocessAction in the web client tries to .map over action.views
and throws TypeError: Cannot read properties of undefined (reading 'map').

Fix: include 'views': [[False, 'form']] alongside view_mode='form' on
both copies of action_open_part (wizard line + sale.order.line).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:40:27 -04:00
gsinghpal
164b775206 feat(views): partner Aerospace group + recipe Certificate Output + cert banner
Three view edits to surface the new cert toggles + workflow nudges:

1. res.partner — Plating Documents tab gains a "Aerospace / Defence"
   separator + group with the three new toggles (Nadcap / MTR /
   Customer-Specific). All boolean_toggle widget, default OFF.

2. fp.process.node — Recipe form gains a "Certificate Output" group
   visible only when node_type == 'recipe'. Five requires_* toggles
   + a blue info banner explaining the suppress-only precedence.

3. fp.certificate — Certificate PDF tab gains a yellow alert banner
   when certificate_type is one of the three orphan types AND no
   attachment is set. Tells the operator "this type expects a PDF
   you upload from disk".

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:05:28 -04:00
gsinghpal
b7211468b2 feat(certificates): orphan-cert attachment gate + render early-return
Block fp.certificate.action_issue on Nadcap / Mill Test / Customer-
Specific certs when attachment_id is empty. These three cert types
are manual-attach only — operator uploads the supplier doc /
regulator-issued cert / filled customer template PDF before the
cert can be issued. Prevents shipping the customer an empty PDF.

_fp_render_and_attach_pdf gets an early-return guard so an orphan-
type cert never tries to render a CoC QWeb template.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T5. Makes test_orphan_cert_issue_blocks_without_attachment pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:04:10 -04:00
gsinghpal
fb6cccc8b1 feat(jobs): three-step cert resolver with recipe suppression
Rewrites fp.job._resolve_required_cert_types as a documented three-step
pipeline:

  Step 1 — partner + part flags (extended to read 3 new orphan-type
           partner toggles: x_fc_send_nadcap_cert / x_fc_send_mill_test
           / x_fc_send_customer_specific)
  Step 2 — recipe-level requires_* Booleans STRIP cert types from
           the wanted set (suppress-only — never adds)
  Step 3 — CoC + thickness bundling preserved (thickness collapses
           into CoC PDF as page 2)

Field-existence guards on partner/recipe attribute reads keep the
resolver robust if the certificates / plating module schemas drift.

Recipe is suppress-only per Q1 locked decision: customer/part is the
ceiling, recipe can only remove. Test 3 (test_recipe_cannot_add_certs_
customer_didnt_want) is the explicit regression guard.

Sub: docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
Task: T4. Makes the 5 resolver tests from T3 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:03:18 -04:00