BuiltEmail workspace

Management-report sharing contract

Implementation: App\Mail\ManagementReportMail, app/Mail/ManagementReportMail.php. Original body: resources/views/emails/management-reports/email.blade.php using mail::message. Caller: app/Http/Controllers/AI/ManagementReportController.php:371–433, sendEmail(). Static references are evidence of usage, not proof of production delivery.

Subject: {{report_name}} (existing source report.name, even when custom body hides default report fields).

Suggested preheader: A management report PDF has been shared with you.

Read docs/integration/shared-footer.md as well for the current footer, native CID image requirements and shared styling.

Portable placeholders

TokenExisting source / derivationHandling
{{logo_url}}Approved original Built app_name PNGOptional deployment override. Concrete approved HTTPS logo is present; otherwise use approved HTTPS or a matching embedded CID.
{{preheader}}Suggested factual attachment summary aboveRequired escaped text. No report approval, accuracy, processing-time or delivery guarantee.
{{business_name}}$this->report->business_nameRequired escaped text in the default body only. Do not silently replace it with constructor businessName.
{{report_name}}$this->report->nameRequired escaped default-body title and existing header subject. Custom body does not remove the subject/filename dependency.
{{period_from}}$report->from->format('d-m-Y')Required existing default-body reporting start, preserving source date semantics. The model casts from to datetime.
{{period_to}}$report->to->format('d-m-Y')Required existing default-body reporting end. Do not substitute another report period format or infer deadlines.
{{sender_name}}$report->user->fullnameRequired default-body Best regards identity. It is the report user (AltUsers relation), not necessarily the authenticated sharing requester.
{{custom_message}}Constructor $body / view body from validated request.body, converted to reviewed equivalent textRequired only in the custom branch. Escape multiline text; preserve paragraphs, meaningful anchor labels/destinations and any author-supplied sign-off. Do not treat arbitrary raw HTML as trusted.

Legacy/duplicate field: constructor businessName is populated with report.business_name and also passed to view data, but this original view does not read businessName. Preserve the existing constructor/view-data contract without creating a second conflicting body token. Header sender email is not a body placeholder, but remains required by the Mailable.

Trigger, access and recipients

routes/api.php:361 POST management-reports/{id}/share calls sendEmail. It obtains request.user(), finds the report by uuid, and allows only the owner except for the controller’s existing default-report exception. Preserve that exact source rule and current authorization; do not assume every report belongs to the authenticated user or add new sharing rights.

An invalid period is repaired through hasValidPeriod/getPeriod and report.update, with the existing error response on failure. The controller accepts emails and cc as arrays or comma-separated strings, de-duplicates/filters them, validates required emails (array, min:1, each email), nullable cc array/email entries, and nullable string body. Do not collapse To/CC, add BCC, replace recipients with the report owner or silently change splitting/trimming semantics.

Mail::to(validated.emails)->cc(validated.cc)->queue(new ManagementReportMail(report, report.business_name, validated.body ?? '')) is explicit queue() evidence at :423–425. Preserve queued behavior and current response/error handling. This is a selected-recipient report-sharing notification, distinct from ReportGenerated/ReportGenerationFailed sent to requesters.

Headers and PDF contract

Subject remains $this->report->name. From and Reply-To both use the restructured report’s user.email and user.fullname. They do not use authenticated request-user data, constructor businessName, configured merchant sender name or the footer’s support address. Keep those source identities even for the default-report access exception.

The Mailable first calls ManagementReportRepository::restructurePages($this->report), then generatePdf($report). restructurePages works on and returns the same ManagementReport instance; it can repair its period and reshape pages, period/account breakdowns and channel defaults. Preserve the same sequence and model behavior rather than replacing the PDF with a static HTML screenshot or moving report calculations into this email.

generatePdf loads user, renders cfo-ai.template and uses Browsershot::html with noSandbox, margins(0,0,0,0), emulateMedia('print'), waitUntilNetworkIdle, format('A3'), landscape and pdf(). Preserve this A3 landscape path and all existing page/data/user dependencies. No PDF or browser-based PDF generation is performed by the library.

Attachment bytes are returned by generatePdf. Keep attachData with filename Str::slug(report.business_name, '_') + '_' + Str::slug(report.name, '_') + '.pdf'. Do not hand-reimplement slugging, replace underscores or use businessName over report.business_name. Do not discard the user relation or report metadata when custom body hides those fields. Shared footer PNGs are separate inline assets, not replacements for the business PDF.

Mutually exclusive body states

Source condition is !empty($body). When PHP-empty, the default message identifies report business/title and from/to dates, then signs off as report.user.fullname. When nonempty, raw $body completely replaces that default message and sign-off. The redesign preserves this replacement behavior; it adds a generic heading and factual attached-PDF reminder, without leaking default fields into a custom message.

Capture original PHP !empty before converting rich text. Literal string 0 is empty and uses default content. Original <p>0</p> is nonempty; normalized text 0 must stay custom. The fixtures use source-only boolean custom_message_visible for this original predicate; it is not a printed token. Do not determine the branch again from normalized text or token-shaped content.

The ready HTML contains two complete marked alternatives. Retain exactly one: default section for original PHP-empty body, custom section otherwise. Remove the other section entirely. Apply the same choice to the marked portable text blocks. When using default, remove the whole custom block rather than leaving an empty {{custom_message}}. In custom mode do not append default business/title/period/Best regards lines. The custom author’s own identity/instructions remain part of the normalized message.

Rich text, escaping and native integration

The original custom branch renders raw {!! $body !!}. The portable draft deliberately uses reviewed equivalent text. Preserve paragraphs/lists and meaningful anchor label plus complete destination through the target adapter; plain strip_tags can lose URLs. If a future integration requires rich HTML, define a separate narrowly allowlisted sanitizer and equivalent text instead of treating untrusted messages as raw markup.

Portable {{placeholder_name}} values are not automatically Blade expressions. Prefer the existing Mailable with a complete HTML Blade view, explicit mapped values/original predicate and separate text view. Preserve subject, sender/Reply-To, To/CC, queue, report restructuring and PDF generation. Do not nest the full document in mail::message or introduce a new templating framework.

Escape all names, dates, report fields, custom text and attributes. If doing string replacement, use one pass; inserted token-looking text remains literal data and must not be interpreted as a template/control flag. There is no action URL or duplicate email link field in this source. Do not add the ReportGenerated notification’s dashboard link or invent recipient access to CFO AI. Validate any overridden image URL separately for approved HTTPS host/path or an actual attached CID, following the shared contract.

Files: public/ready/management-report-sharing.html and .txt; public/previews/management-report-sharing-*.html/.txt. States cover default, custom, PHP-zero default, normalized rich-text zero custom, a normalized reference, long default details and long custom text. No separate project-owned plain-text file was found; the supplied .txt is a proposed equivalent.

This API owns the markup and send path even when report data uses an external accounting channel. Externally authored CRM email remains outside scope. No source model, report, PDF, message, account or real email is created/sent by this design workspace. Browser rendering does not establish Gmail/Outlook/iOS Mail, MIME/PDF correctness or current production delivery. Remaining decisions: target-project normalization/branch mapping, HTML/text registration, asset hosting/CID and actual client testing.