Shop password reset:
- Configured 'shop_customers' password broker on the existing
password_reset_tokens table
- ShopCustomer::sendPasswordResetNotification overrides Laravel default to
send a ShopPasswordResetMail with a tenant-subdomain reset URL
- Routes /shop/password/forgot, /shop/password/email, /shop/password/reset/{token}
+ ShopAuthController showForgotPassword/sendResetLink/showResetPassword/
resetPassword. Forgot view stays generic ("if it exists, we sent…") to avoid
email enumeration. Login view links to "Am uitat parola".
Order confirmation email:
- ShopOrderConfirmationMail + nicely formatted HTML email template
- ShopOrderNotifier::placed now also emails customer_email (best-effort,
warning-only logged on failure) alongside existing Telegram + staff push
Multiple images per Part:
- Part media collection switched from singleFile to multiple (max 8 in form)
- imageUrls() helper for galleries; imageUrl() still returns first for cards
- PartResource form: reorderable multi-upload
- Shop part detail: vertical thumbnails switch the main image via vanilla JS
ShopCustomerResource (tenant Filament, "Magazin" nav group):
- List with name/phone/email/client_id/orders_count/last_login_at
- Edit (no password field exposed)
- "Trimite reset parolă" action uses the new broker
- OrdersRelationManager shows the customer's orders read-only
Tests (7 new):
- forgot sends mail; forgot doesn't disclose unknown email; reset with valid
token changes password; bad token rejected; order email when customer_email
set; email skipped without it; Part has imageUrls() collection
Full suite: 130 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upload an invoice photo → Claude extracts {supplier_name, date, currency,
items, total} as JSON → auto-create a draft Purchase + PurchaseItems →
redirect to edit so the user reviews before confirming/receiving.
OcrInvoiceService:
- Validates supported MIME (jpg/png/webp/gif)
- Reads tenant Claude key (settings.ai.claude_key) — friendly error if missing
- Calls /v1/messages with image content block + structured-output system prompt
- Tolerant parser: strips ```json fences, falls back to first {…} block
- normalize(): computes per-item total when absent, fills overall total
- All return shapes: {ok:bool, data?, error?, raw?, tokens?}
Filament:
- "Import factură (OCR)" header action on Purchases list
- Image file upload → service → matches Supplier by case-insensitive name
(notes the unmapped name if no match) → creates draft Purchase + items →
redirects to the Edit page
Tests (6 new):
- clean JSON parses; markdown fences stripped; malformed → graceful error;
missing key → friendly message + no HTTP; unsupported MIME rejected;
item total computed when missing
Full suite: 123 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Asistent AI chat can now query the tenant DB directly through Claude
tool use. AiToolExecutor exposes 5 read-only tools (search_clients,
get_vehicle, find_parts, recent_workorders, low_stock_parts) all scoped
to the current tenant via BelongsToTenant.
AiAssistantService::callClaude loops on stop_reason=tool_use up to 5 rounds:
- normalize message history to content blocks
- send `tools` definitions + messages to Anthropic API
- on tool_use: execute each tool, append tool_results as user turn, recall
- on end_turn: collect text + cumulative token counts + tool-call audit in
AiMessage.meta.tools
Single-shot helpers (suggestDiagnosis, suggestPrice, vinRecommendations,
suggestParts) are unchanged — only the conversational chat gets tool-use.
Tests (3 new):
- two-round tool_use → execute → final text; verify 5 tools sent both rounds;
cumulative tokens
- executor finds part by brand
- unknown tool name returns error blob
Full suite: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /admin/companies/{id} view page (ViewCompany extends Page) 404'd because
the {record} route param could arrive as a JSON-encoded model (Livewire typed-
property hydration), so findOrFail() received a non-id and threw
ModelNotFoundException. Added resolveRecordKey() to normalize scalar id / model
/ JSON-string down to the integer key.
Also fixed getStats() referencing non-existent parts columns `stock` /
`low_stock_threshold` (real columns are `qty` / `min_qty`), which would 500 the
page once mount resolved.
Added CentralCompanyViewTest as regression (asserts 200 + company name). Full
suite: 100 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WorkOrderCalcTest: vehicle fixture used `brand` (column is `make`, NOT NULL)
and WorkOrderPart used `price`/manual total (column is `sell_price`, total is
auto-computed). Switched to correct columns + valid status.
- ExampleTest: stock test expected 200 on `/` but the central domain redirects
to /admin. Replaced with a deterministic central-root → /admin redirect check.
Full suite now green: 99 passed, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema:
- subcontractors (specialty, rating, contact)
- subcontract_jobs (work_order link, cost, markup_pct, client_price, status
workflow, sent_at/eta/returned_at, paid_to_sub)
Models:
- SubcontractJob: auto number (SC-YY-NNNN), client_price = cost×(1+markup/100)
when markup>0 (else manual), margin() helper, recalcs parent WO on save/delete
- WorkOrder.recalcTotal now includes non-cancelled subcontract job client_price
Filament (new "Subcontractare" nav group):
- SubcontractorResource (specialty/rating CRUD)
- SubcontractJobResource board with cost/client/margin columns + status filters,
nav badge = open jobs
- SubcontractJobsRelationManager on WorkOrder
Tests (7 new):
- client_price from markup; manual price without markup; auto number;
WO total includes jobs; cancelled excluded; delete recalcs; tenant isolation
Closes roadmap to 16/18 stages (only Stage 10 Bodyshop remains).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contextual multipliers layered on top of base MarkupRule pricing, applied
per work-order line based on vehicle, client and urgency.
Schema:
- pricing_coefficients (multiplier, conditions JSON, priority, stackable)
- vehicles.vehicle_class (sedan/suv/commercial/hybrid/ev/premium)
- clients.is_vip
- work_orders.urgency (normal/urgent/express)
PricingEngine::quote(Part, Vehicle?, Client?, urgency):
- base = MarkupRule on buy_price (fallback sell_price or buy×1.30)
- context: class (explicit or inferred hybrid/ev from fuel), age, vip, urgency
- stackable coefficients all multiply; non-stackable take only the highest
- returns {base, final, applied[]} breakdown
PricingCoefficient::matches(ctx) — classes/age range/vip/urgency conditions
(empty = always applies).
Filament:
- PricingCoefficientResource with condition builder (classes, age, vip, urgency)
- vehicle_class select, client is_vip toggle, WO urgency select
- "Preț inteligent" action on WO parts shows breakdown + applies sell_price
Tests (6 new):
- base-only without coefficients; age coefficient gating; VIP; express urgency;
stackable multiply vs non-stackable highest-wins; hybrid inferred from fuel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>