diff --git a/backend/app/api/v1/certificates.py b/backend/app/api/v1/certificates.py index a828afd..0a9afcc 100644 --- a/backend/app/api/v1/certificates.py +++ b/backend/app/api/v1/certificates.py @@ -20,6 +20,7 @@ DonationCertificateUpdate, ) from app.certificates.generator import generate_certificate_pdf +from app.certificates.svg_generator import generate_html_certificate router = APIRouter(prefix="/certificates", tags=["certificates"]) @@ -90,24 +91,22 @@ async def get_public_certificate_page( donor = (await db.execute(select(User).where(User.id == donation.donor_id))).scalar_one_or_none() donor_name = cert.donor_name or (donor.stellar_public_key if donor else None) or "Anonymous Donor" tx_hash = cert.stellar_tx_hash or donation.stellar_tx_hash or "N/A" - html = f""" - - - LINGAP Certificate {cert.id} - -
-

LINGAP On-Chain Donation Proof

-

Public Impact Certificate

-

User Name: {donor_name}

-

Campaign Name: {campaign_name}

-

Milestone: {cert.milestone_description}

-

Donation Amount: {float(cert.amount):,.2f} XLM

-

Transaction Hash: {tx_hash}

-

Verification: {"Verified" if cert.verified else "Pending"}

-
- - - """ + html = generate_html_certificate( + donor_name=donor_name, + amount=float(cert.amount), + beneficiary_name=campaign_name, + milestone_description=cert.milestone_description or "Campaign milestone completed", + lives_touched=int(cert.lives_touched or 0), + total_donated=float(cert.total_donated or cert.amount), + donation_date=donation.created_at, + stellar_tx_hash=tx_hash, + merkle_proof=cert.merkle_proof, + onchain_hash=cert.onchain_hash, + certificate_id=str(cert.id), + block_number=None, + network="Stellar Mainnet" if cert.verified else "Pending Verification", + certifying_officer="LINGAP Foundation", + ) return HTMLResponse(content=html) @@ -264,6 +263,7 @@ async def list_certificates( "donation_id": str(cert.donation_id), "donor_name": cert.donor_name, "amount": float(cert.amount), + "asset": donation.asset, "beneficiary_name": cert.beneficiary_name, "campaign_name": campaign_name, "milestone_description": cert.milestone_description, diff --git a/backend/app/certificates/svg_generator.py b/backend/app/certificates/svg_generator.py index 4178365..2a2e46b 100644 --- a/backend/app/certificates/svg_generator.py +++ b/backend/app/certificates/svg_generator.py @@ -1,4 +1,4 @@ -"""Enhanced HTML certificate generation for LINGAP with PNG fallback wrapper.""" +"""Enhanced HTML certificate generation for LINGAP with PNG fallback wrapper.""" from datetime import datetime import hashlib @@ -19,46 +19,24 @@ def generate_html_certificate( network: str = "Stellar Mainnet", certifying_officer: str = "LINGAP Foundation", ) -> str: - """Generate a polished HTML certificate for a LINGAP donation. - - Args: - donor_name: Full name of the donor - amount: This donation amount in PHP - beneficiary_name: Name of the campaign beneficiary - milestone_description: Milestone reached (e.g. "Chemo Cycle 3 of 6 Completed") - lives_touched: Number of lives impacted - total_donated: Cumulative amount donated by this donor - donation_date: Date of the donation - stellar_tx_hash: Full Stellar blockchain transaction hash - merkle_proof: Optional Merkle proof string - onchain_hash: Optional on-chain hash reference - certificate_id: Optional certificate ID (auto-generated if not provided) - block_number: Optional Stellar ledger block number - network: Blockchain network name (default: "Stellar Mainnet") - certifying_officer: Name for the signature block - - Returns: - Full standalone HTML document as a string - """ + """Generate a polished HTML certificate for a LINGAP donation.""" date_str = donation_date.strftime("%B %d, %Y") tx_preview = stellar_tx_hash[:20] + "..." merkle_preview = (merkle_proof[:20] + "...") if merkle_proof else "N/A" onchain_preview = (onchain_hash[:20] + "...") if onchain_hash else "N/A" - # Auto-generate certificate ID from tx hash if not provided if not certificate_id: short = hashlib.sha256(stellar_tx_hash.encode()).hexdigest()[:6].upper() year = donation_date.year certificate_id = f"LNG-{year}-{short}" - block_display = f" · Block {block_number}" if block_number else "" + block_display = f" · Block {block_number}" if block_number else "" - # Build the 4 verification statement lines verification_statements = [ "Blockchain transaction verified on Stellar Mainnet", "Beneficiary campaign identity independently confirmed", "Funds disbursed directly to verified medical providers", - "Immutable record — publicly accessible at any time", + "Immutable record - publicly accessible at any time", ] statements_html = "\n".join( f""" @@ -79,86 +57,132 @@ def generate_html_certificate( - - + + - LINGAP Certificate — {certificate_id} + LINGAP Certificate - {certificate_id} + + +
+
-
- - - + L
@@ -556,23 +643,26 @@ def generate_html_certificate(
-
+
+
+
+
+
+
+
-
This certifies that
{donor_name}
has made a verified, blockchain-recorded donation in support of a humanitarian cause
-
Verified Donation
₱{amount:,.2f}
- -
Benefiting the campaign of
+
in benefit of the campaign of
{beneficiary_name}
@@ -581,13 +671,12 @@ def generate_html_certificate(
-
@@ -602,7 +691,7 @@ def generate_html_certificate(
@@ -615,7 +704,7 @@ def generate_html_certificate(
@@ -629,13 +718,13 @@ def generate_html_certificate(
-
Stellar Blockchain Verification
-
● VERIFIED
+
● VERIFIED
+
Transaction Hash
@@ -656,54 +745,51 @@ def generate_html_certificate(
-
-
- + d="M 44,44 m -31,0 a 31,31 0 1,1 62,0 a 31,31 0 1,1 -62,0"/> + + + + - - - + + - - - + points="44,6 52,30 77,30 57,46 65,70 44,55 23,70 31,46 11,30 36,30" + fill="none" stroke="#10B981" stroke-width="1.2" opacity="0.2"/> + + font-family="DM Sans,Helvetica,Arial,sans-serif" + font-weight="700" letter-spacing="2.8" fill="#059669"> - LINGAP · VERIFIED · IMPACT · + LINGAP · VERIFIED · IMPACT · - - CERTIFIED - DONOR + CERTIFIED + DONOR
-
{statements_html}
-
Lingap
@@ -713,19 +799,18 @@ def generate_html_certificate(
-
+
-
-
+
""" @@ -737,10 +822,6 @@ def get_certificate_hash(html_content: str) -> str: return hashlib.sha256(html_content.encode("utf-8")).hexdigest() -# --------------------------------------------------------------------------- -# Backwards-compatible SVG wrapper (kept for reference; use HTML version above) -# --------------------------------------------------------------------------- - def generate_svg_certificate( donor_name: str, amount: float, @@ -753,12 +834,7 @@ def generate_svg_certificate( merkle_proof: str | None = None, onchain_hash: str | None = None, ) -> str: - """Legacy SVG certificate generator (thin wrapper around the HTML version). - - Embeds the full HTML certificate inside an SVG foreignObject so it can be - used anywhere an SVG is expected (e.g. stored as .svg, embedded in ). - For most use-cases, prefer generate_html_certificate() directly. - """ + """Legacy SVG certificate generator wrapper around HTML template.""" html = generate_html_certificate( donor_name=donor_name, amount=amount, @@ -771,7 +847,7 @@ def generate_svg_certificate( merkle_proof=merkle_proof, onchain_hash=onchain_hash, ) - svg = f""" + return f""" @@ -781,19 +857,14 @@ def generate_svg_certificate(
""" - return svg - -# --------------------------------------------------------------------------- -# Example usage / smoke test -# --------------------------------------------------------------------------- if __name__ == "__main__": sample = generate_html_certificate( donor_name="Cameron Graham", amount=7_498.00, - beneficiary_name="Gino Reyes — Home Repair, Manila", - milestone_description="Milestone 1: Materials Procurement — 50% Funded", + beneficiary_name="Gino Reyes - Home Repair, Manila", + milestone_description="Milestone 1: Materials Procurement - 50% Funded", lives_touched=6, total_donated=7_498.00, donation_date=datetime(2026, 5, 17), diff --git a/frontend/src/app/(marketing)/certificate/page.tsx b/frontend/src/app/(marketing)/certificate/page.tsx index 726bdd2..096636b 100644 --- a/frontend/src/app/(marketing)/certificate/page.tsx +++ b/frontend/src/app/(marketing)/certificate/page.tsx @@ -70,7 +70,7 @@ export default function CertificatePage() { const mapped: Certificate[] = data.map((c: any) => ({ id: c.id, donor: c.donor_name || user?.name || "Anonymous Donor", - amount: `₱${parseFloat(c.amount || "0").toLocaleString(undefined, { minimumFractionDigits: 2 })}`, + amount: (() => { const n = parseFloat(c.amount || "0").toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const a = (c.asset || "XLM").toUpperCase(); return a === "PHP" ? `\u20b1${n}` : `${n} ${a}`; })(), campaign: c.campaign_name || c.milestone_description || "Campaign Milestone", institution: c.beneficiary_name || "LINGAP Verified Network", milestone: c.milestone_description || "Milestone Complete", @@ -276,3 +276,6 @@ export default function CertificatePage() {
); } + + +