Coverage for functions \ flipdare \ payments \ receipt_generator.py: 98%
94 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python3
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13from fpdf import FPDF
14from flipdare.app_log import LOG
15from flipdare.constants import COMPANY_ADDRESS, COMPANY_LOG_MED, IS_DEBUG
16from flipdare.payments.payment_types import AppReceipt
17from flipdare.util.public.public_url_factory import PublicUrlFactory
18from flipdare.backend.app_storage_client import AppStorageClient
19from flipdare.util.time_util import TimeUtil
21__all__ = ["ReceiptGenerator"]
24class ReceiptGenerator:
25 def __init__(self, storage_client: AppStorageClient) -> None:
26 self.client = storage_client
28 def generate(self, info: AppReceipt) -> str | None:
29 try:
30 if IS_DEBUG:
31 msg = (
32 f"Generating receipt PDF for receipt_id '{info.receipt_id}' with info: {info}"
33 )
34 LOG().debug(msg)
36 pdf_bytes = self._create_receipt(info)
37 content = PublicUrlFactory.create_receipt_content(info, pdf_bytes)
38 client = self.client
39 perm_url = client.generate_public_url(content)
41 if IS_DEBUG:
42 msg = f"Generated receipt URL for receipt_id '{info.receipt_id}': {perm_url}"
43 LOG().debug(msg)
45 return perm_url
46 except Exception as e:
47 LOG().error(f"Error generating receipt for receipt_id '{info.receipt_id}': {e}")
48 return None
50 def _create_receipt(self, receipt_data: AppReceipt) -> bytes: # noqa: PLR0915
51 pdf = FPDF()
52 pdf.add_page()
54 customer_id = receipt_data.customer_id
55 account_id = receipt_data.account_id
56 receipt_id = receipt_data.receipt_id
57 dare_id = receipt_data.dare_id
58 dare_description = receipt_data.dare_description
59 amount = receipt_data.amount
60 currency_code = receipt_data.currency_code
62 date_str = TimeUtil.formatted_user_accurate(receipt_data.receipt_date)
64 # --- Header & Logo ---
65 logo = COMPANY_LOG_MED
66 use_logo = True
67 if not logo.exists():
68 LOG().warning(f"Company logo not found at {logo}. Not using logo.")
69 use_logo = False
71 if use_logo:
72 pdf.image(logo, x=12, y=8, w=30)
74 pdf.set_font("helvetica", "B", 24)
75 pdf.cell(0, 10, "PAYMENT RECEIPT", align="R", new_x="LMARGIN", new_y="NEXT")
76 pdf.ln(10)
78 # Add Business Address/Description
79 pdf.set_font("helvetica", "", 10)
80 pdf.multi_cell(0, 5, COMPANY_ADDRESS, align="R")
82 # dynamic position for the horizontal line and metadata section based on content height
83 start_y = pdf.get_y() + 10
85 # Draw a Horizontal Line for styling
86 pdf.line(10, start_y - 5, 200, start_y - 5)
88 # --- 2. Two-Column Metadata Layout ---
89 pdf.set_font("helvetica", "B", 10)
90 pdf.set_text_color(100, 100, 100)
92 # Left Column
93 pdf.set_xy(10, start_y)
94 pdf.cell(95, 5, "BILLED TO:", new_x="LMARGIN", new_y="NEXT")
95 # ... (rest of left column)
97 # Right Column
98 pdf.set_xy(110, start_y)
99 pdf.set_font("helvetica", "B", 10)
100 pdf.set_text_color(100, 100, 100)
101 pdf.cell(90, 5, "RECEIPT DETAILS:", align="R", new_x="LEFT", new_y="NEXT")
103 pdf.set_font("helvetica", "", 10)
104 pdf.set_text_color(0, 0, 0)
105 pdf.set_x(110)
106 pdf.cell(90, 5, f"Date: {date_str}", align="R", new_x="LEFT", new_y="NEXT")
107 pdf.set_x(110)
108 pdf.cell(90, 5, f"Receipt #: {receipt_id}", align="R", new_x="LEFT", new_y="NEXT")
110 pdf.ln(5) # 5mm gap
111 pdf.set_font("helvetica", "", 10)
112 pdf.set_text_color(0, 0, 0)
114 # customer info
115 # Define a fixed label width (e.g., 30mm)
116 is_vip = receipt_data.fee_type.is_vip
117 label_w = 22
118 value_w = 65
120 # Line 1
121 pdf.cell(label_w, 5, "Name")
122 name = f": {receipt_data.first_name} {receipt_data.last_name}"
123 if is_vip:
124 name += " - (V.I.P)"
125 pdf.cell(value_w, 5, name, new_x="LMARGIN", new_y="NEXT")
127 # Line 2
128 pdf.cell(label_w, 5, "Customer ID")
129 pdf.cell(value_w, 5, f": {customer_id}", new_x="LMARGIN", new_y="NEXT")
130 # Line 3
131 pdf.cell(label_w, 5, "Account ID")
132 pdf.cell(value_w, 5, f": {account_id}", new_x="LMARGIN", new_y="NEXT")
134 # spacing
135 pdf.ln(20)
137 # --- 3. The "Dare" Description (Main Item) ---
138 # We use a light grey box to highlight the "Dare" content
139 pdf.set_fill_color(245, 245, 245)
140 pdf.set_font("helvetica", "B", 12)
141 pdf.cell(
142 0,
143 10,
144 f" Item / Description (Product ID: {dare_id})",
145 fill=True,
146 new_x="LMARGIN",
147 new_y="NEXT",
148 )
150 pdf.ln(5) # 5mm gap
151 # Stripe amounts in cents, so we divide by 100
152 # use ISO 4217 Style (Formal) for currency code display
153 display_amount = f"{currency_code.name} {(amount / 100):.2f}"
155 pdf.set_font("helvetica", "", 11)
156 # Print the description (width 0 to fill, but we stop 'NEXT' to stay on line)
157 # We use new_x="RIGHT" to keep the cursor at the end of this cell
158 pdf.cell(0, 10, dare_description, border="B", new_x="RIGHT", new_y="TOP")
160 # Now move the cursor back to the right margin to overlay the price on the same line
161 # We use align="R" so the price sits at the far right of that bottom border
162 pdf.cell(0, 10, display_amount, align="R", new_x="LMARGIN", new_y="NEXT")
164 # --- 4. Total Amount ---
165 pdf.set_font("helvetica", "B", 16)
166 pdf.cell(0, 15, f"TOTAL PAID: {display_amount}", align="R", border="T")
168 pdf_data = pdf.output()
169 return bytes(pdf_data)