- New Help article at /Help/CustomerIntakeKiosk covering setup, in-person
and remote intake flows, what happens on submission, reviewing intakes,
and troubleshooting (signature pad, connection issues, seed data)
- Add kiosk entry to _HelpNav under Operations
- Update HelpKnowledgeBase: nav overview, full kiosk section, two new
common workflow entries (walk-in kiosk and remote intake)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix Jobs.Id FK violation: save job first with CompleteAsync() to get
its DB-assigned Id, THEN set session.LinkedJobId and save again.
Previously job.Id was still 0 when written to the nullable FK column.
- Replace ?? 1 fallbacks on JobStatusId/JobPriorityId with explicit
InvalidOperationException — hardcoded 1 may not exist in the company's
lookup tables; now fails loud with a clear message instead of an FK error.
- Add ValidateSessionState check to Terms POST so expired/already-submitted
sessions don't re-run ProcessSubmissionAsync and create duplicate jobs.
- Null-guard session.JobDescription before slicing for notification snippet.
- Tighten catch block: wrap the fallback CompleteAsync in its own try/catch
so a secondary failure doesn't mask the original error in logs.
- Swap Job.Description / SpecialInstructions: Description now holds the
actual job description text; SpecialInstructions records the intake source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ProcessSubmissionAsync was creating a Job without JobPriorityId, leaving
it as 0 which violates the FK to JobPriorityLookups. Look up the NORMAL
priority the same way JobsController does everywhere else.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Compress photos client-side before uploading (1200px max, JPEG 85%):
full-res phone photos (5-15 MB) → ~150-250 KB, dramatically reducing
upload time on slow mobile connections and Anthropic processing time
- Add 120s AbortController to both AiAnalyzeItem fetch calls so a stalled
mobile connection produces a clear 'timed out' error instead of spinning forever
- After 30s show 'Still analyzing… this can take a minute on mobile' to
reassure users the request is in progress
- Reset loading text on retry so the slow-connection hint doesn't persist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_KioskLayout inactivity timer now reads ViewBag.InactivityTimeoutMs
(defaults to 5 min). PopulateKioskViewBagFromSession sets it to 45 s
on every intake step so an abandoned form auto-returns to the waiting
screen. Welcome screen and Confirmation page are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Download signature_pad 4.1.7 to wwwroot/lib/signature-pad/ to eliminate
CDN SRI hash failures and network dependencies on the tablet
- Wrap resizeCanvas in requestAnimationFrame so offsetWidth is non-zero
when measured (browser layout pass must complete first)
- Add guard for SignaturePad not defined (shows user-visible error instead
of silent JS crash)
- Add scrollIntoView on signature validation error for better tablet UX
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SW fetch() wraps SSE responses in a buffered Response, preventing SignalR
streaming — handshakes time out after 15s as a result. Exclude /hubs/ and
/Kiosk/PollSession so the browser handles them directly without SW wrapping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SignalR WebSocket and SSE both receive immediate 'Handshake was canceled' from the
server-side hub context. The 15-second delay between negotiate and SSE connect
reveals the handshake timer has expired before the transport opens — caused by Azure
App Service's ingress proxy resetting anonymous long-lived connections.
Replacement: /Kiosk/PollSession (anonymous GET, no-cache) queried every 3 seconds.
Returns the most recent Active InPerson session created in the last 60 seconds.
The kiosk navigates when hasSession=true. Status dot: gray->green on first success,
yellow on network error, blue when navigating. Removed signalr.min.js from kiosk layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. kiosk-welcome.js: force SSE|LongPolling transport on the kiosk hub.
Azure App Service's ingress proxy cancels anonymous WebSocket handshakes
before the SignalR protocol exchange completes. SSE and long polling
work fine for the low-frequency StartIntake push this hub needs.
2. SubscriptionMiddleware: add /hubs/ and /Kiosk/ to SkipPaths so a
subscription redirect can never fire on a hub or kiosk request and
abort the connection mid-handshake.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs identified:
1. Routing: /Kiosk/Intake/{token}/{action} had no matching route — 4-segment
URL fell through the default 3-segment {controller}/{action}/{id?} route.
Added explicit kiosk_intake route in Program.cs.
2. View names: Contact/Job/Terms/Confirmation actions returned View(model)
which resolved to Views/Kiosk/{Action}.cshtml — those files don't exist.
Views live in Views/Kiosk/Intake/. Fixed all six return statements.
3. Diagnostics: conn dot now starts gray ("Connecting...") and turns green
only when SignalR actually connects. Red + message if no company ID or
connection fails. Makes it easy to confirm the hub connection is live.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Layout rendered a small logo at top of every kiosk page. Welcome.cshtml
also rendered its own centered logo, resulting in two logos. Suppress
the layout logo on the Welcome screen via HideLayoutLogo ViewBag flag.
Bump kiosk-welcome-logo from 120x280 to 200x420 for better presence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, data-company-id was empty and the JS connected to kiosk- (no ID),
so StartSession signals never reached the tablet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Start Intake button only shows when company has an active kiosk token
- Remote Link button renamed to "Send Intake Link" for clarity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CompanySettings/Logo requires tenant context and fails on anonymous
kiosk pages. Added Kiosk/Logo which resolves the company from the
KioskDevice cookie and proxies the blob directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stripe payments/refunds/chargebacks now post DR/CR entries (PaymentController)
- Vendor credit void now reverses the posted GL lines (VendorCreditsController)
- Gift certificate issue/redeem/void post GL to account 2500 GC Liability;
FinancialReportService Trial Balance + Balance Sheet include GC liability and
breakage income; P&L shows deferred revenue deduction and breakage income line
- Customer deposits now post DR Checking / CR 2300 on record, reverse on delete;
invoice auto-apply uses DR 2300 / CR AR (not a second bank debit); draft
invoice delete reverses deposit-apply GL before the AR reversal
- Deposit.DepositAccountId column added; account 2300 seeded via migration
- InvoicesController.ApplyCredit now posts DR Sales Discounts / CR AR,
consistent with CreditMemosController.Apply
- IssueRefund (cash/card) posts DR AR / CR Bank and sets Refund.DepositAccountId;
refund modal gains a bank account selector hidden for store-credit path
- CancelRefund (cash/card) reverses the IssueRefund GL entries
- LedgerService GetAccountLedgerAsync + ComputePriorBalanceAsync now include
Refunds, CreditMemoApplications, VendorCreditApplications, GC Liability (2500),
and Customer Deposits (2300) so account ledger view and RecalculateAllAsync
produce correct balances
- Three EF migrations applied: SeedSalesDiscountsAccount, AccountingGapsPhase2,
AccountingDepositsGL
- Unit tests updated for new IAccountBalanceService constructor params (200/200)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The two floating badges in the Onboarding > Setup Progress card had no
context — "Not Started" and a path name appeared without explanation.
Added inline "Status:" and "Path:" labels so both badges are immediately
readable without needing a tooltip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Companies/Index:
- Added Health badge column (Healthy / At Risk / Critical / Never Active)
with the numeric score in a tooltip; computed from the same signals as
CompanyHealth/Index using the new shared CompanyHealthHelper
Companies/Details:
- Converted flat card layout to five tabs: Overview, Users, Subscription,
Onboarding, Health; URL hash is preserved so the active tab survives
page refresh and back navigation
- Subscription tab shows plan/status/dates with an expiry countdown and a
"Manage Subscription & Features" button to the full Manage page
- Onboarding tab shows wizard completion, milestone progress bar, and
first-activity dates (previously only on the standalone page)
- Health tab shows score gauge, risk badge, and individual risk signals
with a link through to the full CompanyHealth dashboard
- JS moved to wwwroot/js/companies-details.js (avoids inline-script failures)
Infrastructure:
- Extracted ComputeHealth / ToRiskLevel / ChurnRisk to CompanyHealthHelper.cs
(same Controllers namespace); CompanyHealthController delegates to it
- CompanyCountSummary extended with Jobs30Counts, Jobs90Counts, LastLoginDates
(3 extra GROUP BY queries scoped to the current page IDs, not all companies)
- CompanyListDto gains HealthScore, HealthRisk, LastLoginDate
Navigation:
- Removed "Onboarding Progress" hub card from People & Activity; the data
is now surfaced directly on the Companies/Details Onboarding tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a direct link to UserActivity/Online immediately below the People
& Activity hub entry so the current active-user count is visible at a
glance without navigating into the hub first. The count badge is
rendered using the already-injected OnlineUserTracker.GetActiveCount()
call and is hidden when the count is zero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace true/false text display with Yes/No radio button groups for
boolean platform settings; toggling auto-submits the form so no Edit
modal is needed for flags
- IsBool() helper detects *Enabled, *AppliesToTrials, *TrialsEnabled keys
- Hide Edit button for boolean settings (radio buttons are the control)
- Add AI group icon (bi-robot) and description to the group header switch
- Add Max key detection to InputType/InputHint for number inputs
- Migration AddMissingPlatformSettings seeds 6 previously missing rows
(SmsEnabled, TrialsEnabled, GracePeriodDays, GracePeriodAppliesToTrials,
MaxTenants, AiCatalogPriceCheckEnabled) using IF NOT EXISTS guards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Section headers now show a group-specific icon, colored icon tile, and a one-line description explaining what each group controls (General, Notifications, Subscriptions, Quotes, Data Retention)
- Each setting now displays UpdatedAt and UpdatedBy metadata below the current value so operators can see when and by whom a setting was last changed
- Edit modal now uses type-appropriate inputs: number (with min=0, step=1) for *Days keys, email for *Email keys, url for BaseUrl, text otherwise; each type shows a contextual hint
- Key name shown in monospace below the label on desktop for operator reference
- Added SuccessMessage TempData alert at the top of the page
- No backend or DB changes — view-only redesign
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SeedData/Index: added prominent danger banner when running in Production (environment include="Production") so operators are clearly warned before writing to the live database; page remains accessible since seeding is occasionally valid in prod for new company onboarding
- StorageMigration/Index: added warning banner in Production explaining the tool is not needed now that Azure Blob migration is complete
- PlatformAdminController: hide Storage Migration hub card in Production via ShowStorageMigration flag (same WEBSITE_SITE_NAME pattern as ShowRawLogFiles); Seed Data card remains visible so prod onboarding stays reachable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added an Observability back-link at the top of all 7 Observability-area pages (AuditLog, SystemLogs, SystemInfo, AiUsageReport, UsageQuota, BannedIps, Diagnostics/Index) consistent with the Maintenance back-link pattern from PR 2
- Renamed company-level nav label and view title from "Notification Log" to "Email & SMS Log" to distinguish it clearly from the platform-level "Platform Notifications", "Audit Log", and "System Logs" surfaces
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DiagnosticsController: replaced raw [Authorize(Roles = "SuperAdmin,Administrator")] with [Authorize(Policy = SuperAdminOnly)] to match every other platform-admin controller; added PowderCoating.Shared.Constants using directive
- DataPurge, DataExport, StorageMigration, SeedData: added "← Maintenance" breadcrumb link at the top of each page so operators know they are in the guarded maintenance area and can navigate back to the hub
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- QuotePricingAssemblyService: catalog no-coat items now respect PowderCostOverride instead of always using DefaultPrice
- PricingCalculationService: removed duplicate catalog fast-path in CalculateQuoteTotalsAsync that bypassed CalculateQuoteItemPriceAsync (and thus ignored PowderCostOverride); all items now go through the single authoritative path
- Jobs/Details.cshtml: timeTracking.del() and timeTracking.save() now call costing.load() after success so the time entry row and costing breakdown stay in sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- add grouped platform admin hub pages, view models, and shared card UI\n- simplify the super admin nav and dashboard quick links around the new hubs\n- fix the AiQuoteService EstimatedMinutes assignment so the infrastructure project builds cleanly
Two bugs caused AI estimates to collapse to the shop minimum floor:
1. Coating rate with no guard: when a shop hadn't calibrated their
coating gun (rate = 0), the prompt injected '~0 sqft/hr' paired
with 'MUST use shop-specific rates' — Claude returned near-zero
estimatedMinutes, zeroing labor cost and triggering the floor.
Fixed to mirror the existing blast-rate guard: rate=0 now sends
a fallback instruction to use conservative industry-average times.
2. Per-item minutes divided by quantity: both the system prompt and
user prompt explicitly tell Claude to return estimatedMinutes 'per
single item', but CalculatePricingPreview() was dividing by qty
anyway. For qty > 1 this halved (or more) the labor cost, again
pushing toward the floor. Removed the incorrect divide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Customer validation now accepts Mobile Phone as a valid contact method;
previously only Email or Phone satisfied the requirement
- Create customer hint text updated to match the new validation rule
- Pagination page-size dropdown gets min-width: 5rem so the number has
breathing room from the caret arrow across all paginated lists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Quotes index now excludes Converted status by default so the list
stays focused on active work. Converted quotes remain accessible via
the status filter dropdown. Selecting any explicit status filter
(including Converted) bypasses the exclusion as expected.
Also consolidated the two GetQuoteStatusLookupsAsync calls into one
at the top of the action since the cache makes it free.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 — Invoice total didn't match job total for direct jobs:
- Root cause: all three item-save paths in JobsController passed null for
ovenCostId, so FinalPrice/ShopSuppliesAmount were stored without oven cost
while the Details page recalculated live with OvenCostId and showed higher.
- Add OvenBatchCost stored field to Job entity (migration AddJobOvenBatchCost,
default 0 for existing rows).
- Fix Create, Edit, and UpdateItems to pass job.OvenCostId and save OvenBatchCost.
- Fix InvoicesController.Create GET for direct jobs to use stored OvenBatchCost
and ShopSuppliesAmount as separate labeled lines instead of recalculating
shop supplies from scratch (which excluded the oven cost base).
Bug 2 — Quote status stayed Draft after "Send Quote via Email":
- ResendQuote advanced the approval token and sent the email but never
updated the status. Added Draft → Sent advancement (same guard used by
the SMS send path) so the status updates on successful email send.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added form-text blurbs under every permission checkbox on both pages
so admins know exactly what each permission unlocks at a glance
- Replaced single Accountant default with a full roleDefaults map covering
Viewer, Worker, Accountant, and Manager roles
- Create page applies defaults on load and on role change (fresh form)
- Edit page preserves saved permissions on load; only resets to defaults
when the role is explicitly changed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings.cshtml: add Accountant to roles table with description; add
Fine-Grained Permissions subsection with a full table of all 16 permissions
including the new Can Manage Bills & AP and Can Manage Accounting entries
- HelpKnowledgeBase.cs: add Accountant to ROLE AWARENESS section at top;
add Accountant to USER MANAGEMENT roles list with auto-checked permissions
note; add Fine-grained permissions paragraph documenting CanManageBills,
CanManageAccounting, and Accountant role defaults
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AppConstants: add Accountant to CompanyRoles; add CanManageBills and
CanManageAccounting to Policies
- ApplicationUser: add CanManageBills and CanManageAccounting bool fields
- UserManagementDtos: expose new fields in all three DTOs
- ClaimsPrincipalFactory: emit ManageBills and ManageAccounting claims
- Program.cs: add CanManageBills and CanManageAccounting policies;
update CanManageInvoices, CanViewReports, CanManagePurchaseOrders,
and CanManageVendors to auto-pass for Accountant role
- BillsController: replace CanManageInventory with CanManageBills on
all write actions (correct policy — bills are not inventory)
- BankReconciliationsController: replace CanManageJobs with
CanManageAccounting on write actions
- CompanyUsersController: add Accountant to validCompanyRoles (both
Create/Edit), legacyRole switch, and all permission assignment blocks
- Create/Edit views: add Accountant option to role dropdown; add
CanManageBills and CanManageAccounting checkboxes; JS auto-checks
financial permissions when Accountant role is selected
- Migration AddAccountantRolePermissions: adds columns + backfills
CanManageBills=1 and CanManageAccounting=1 for all CompanyAdmin users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reports.cshtml: added AI Payment Risk Prediction, Ask Your Financials,
and Bank Rec Auto-Match subsections under AI-Powered Reports; updated
on-this-page nav with sub-links for all three
- AccountsPayable.cshtml: added Recurring Bill Detection section explaining
pattern cards, frequency types, confidence badges, next expected date,
and the 2-occurrence minimum
- HelpKnowledgeBase.cs: added Recurring Bill Detection to BILLS section;
added AI Payment Risk Prediction and Ask Your Financials to REPORTS
available-reports list; added features 12–15 to AI FEATURES section
(Recurring Detection, Payment Risk, Financial Query, Bank Rec Auto-Match)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feature 7: Bank Rec Auto-Match — AiSuggestMatches endpoint scores uncleared
transactions vs statement ending balance; AI Auto-Match panel in Reconcile.cshtml
with confidence highlights and Apply All button.
Feature 8: Late Payment Prediction — PredictLatePayments endpoint scores open AR
customers by risk (high/medium/low) using historical avg-days-to-pay + late rate;
rendered as badge table in AR Aging view via ar-aging-ai.js.
Feature 9: Natural Language Financial Queries — FinancialQuery GET page + RunFinancialQuery
POST; 12-month context snapshot pre-loaded; answers grounded in real data with
supporting facts, follow-up suggestions, session history, and example chips.
Feature 10: Recurring Bill Detection — RunRecurringDetection scans 12 months of bills
for vendor payment patterns (monthly/quarterly/annual); card grid view in Bills/RecurringDetection.cshtml
with confidence badges, next-expected-date, and suggested actions.
Supporting: 4 new DTO groups in AccountingAiDtos.cs, 4 method signatures in
IAccountingAiService.cs, 4 implementations in AccountingAiService.cs, 4 new
AiFeatures constants, 2 new Landing page AI report cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Templates button sits alongside Board and Blank Work Order in the
Jobs index action bar. Nav item and the now-redundant "& Templates"
section title are removed, trimming one more item from the sidebar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace button-style strip with proper underline tabs — active side gets
an ember bottom-border matching the sidebar's existing active-link
language. Add scrollbar-gutter: stable so the sidebar interior width
does not shift when the scrollbar appears/disappears between modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-tab strip between Dashboard and the Operations section lets users
toggle between the shop-management view and the accounting view.
FOUC-prevention: server-side controller detection stamps data-nav-mode
on <html> before first paint; CSS hides the inactive side instantly.
JS (nav-mode.js) auto-switches to Finance when navigating to an
accounting controller, restores saved preference everywhere else.
Vendors appears in both modes; all 39 nav items carry data-nav tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Budgeting:
- Budget + BudgetLine entities with Jan–Dec monthly columns per GL account
- BudgetsController: Index, Create, Edit, SetDefault, Copy, Delete
- Copy action rolls a budget forward to a new fiscal year
- Budget vs. Actual report (BudgetVsActual): compares monthly budget amounts to
real P&L by calling GetProfitAndLossAsync once per month; variance shown as
favorable/unfavorable; year + budget selectors in header
- Views: Budgets/Index, Create, Edit with inline annual totals via budget-edit.js
- Nav link + report card on Landing
Year-End Close:
- YearEndClose entity records each closed year + JE reference for audit trail
- AccountsController.YearEndClose GET (history + form) + CloseYear POST
- Close zeroes all Revenue and Expense/COGS account balances into Retained Earnings
via IAccountBalanceService and posts a supporting JE dated Dec 31
- Idempotency: rejects attempt to close an already-closed year
- Pre-close checklist in view to guide the workflow
- Nav link under Finance
Migration AddBudgetsAndYearEndClose applied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Invoice Write-Off: WriteOff POST action in InvoicesController posts bad-debt JE
(DR bad debt expense / CR AR), reduces customer balance, marks invoice WrittenOff;
write-off modal added to Invoice Details view with expense account selector
- Fixed Assets: FixedAsset + FixedAssetDepreciationEntry entities with straight-line
depreciation; FixedAssetsController (Index/Create/Edit/Details/PostDepreciation/Delete);
PostDepreciation auto-generates one JE per asset per period, skips already-posted,
fully-depreciated, and disposed assets; full CRUD views + nav link
- Period Locking: Company.BookLockedThrough field; AccountingPeriodValidator static helper;
lock check added to JE Post and Bill Create (blocks backdating into closed periods);
SetPeriodLock action + date picker UI in Company Settings Accounting section
- 1099 Tracking: Is1099Vendor flag on Vendor entity + DTOs; checkbox in Create/Edit views;
TaxReporting1099 report action + view lists payments by year, flags vendors >= $600;
report card added to Reports Landing
- Migration AddFixedAssetsLockAnd1099 applied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CreditMemosController with Index, Details, Create, Apply, and Void actions.
All business logic (atomic apply transaction, RemainingBalance cap,
customer.CreditBalance adjustment, auto-Paid invoice when BalanceDue hits zero)
mirrors the invoice-centric IssueCreditMemo/ApplyCredit/VoidCreditMemo actions in
InvoicesController but redirects back to the credit memo rather than an invoice.
Views: Index (stats bar, status+search filter, table), Details (two-col layout
with application history table and Bootstrap Apply/Void confirm modals),
Create (customer dropdown, amount, reason, notes, optional expiry).
Apply modal populates amount automatically from min(remaining credit,
invoice balance due) via credit-memo.js data-attribute wiring (no inline scripts).
Nav: Credit Memos added to Billing & Payments section in _Layout.
Build: 0 errors. Unit tests: 200/200.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop orphan VendorCreditId1 column from VendorCreditApplications (was
scaffolded by EF because WithMany() lacked inverse navigation name;
fixed WithMany() → WithMany(vc => vc.Applications) in ApplicationDbContext)
- Wire EarlyPaymentDiscount fields through full data path: added
EarlyPaymentDiscountPercent/Days to CreateInvoiceDto, hidden inputs to
Invoice Create view, and JS to populate from customer AJAX response
- Add missing [HttpGet] attribute to TaxRatesController.Index
- Document GenerateNow architecture exception with XML rationale
Migration DropOrphanVendorCreditId1 applied. Build: 0 errors, 168 warnings.
Unit tests: 200/200 passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>