Initial project upload

This commit is contained in:
Mohamed Mathar Irfan
2026-07-28 17:57:02 +05:30
commit ed6610d5d8
23919 changed files with 3003316 additions and 0 deletions

View File

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,59 @@
"""Transactional email via AWS SES, with a console mock fallback."""
from typing import Optional
from app.core.config import settings
from app.models import EmailLog
async def _log(to_email: str, subject: str, template: str, status: str = "sent", error: Optional[str] = None):
try:
await EmailLog(to_email=to_email, subject=subject, template=template,
status=status, error=error).insert()
except Exception:
pass
async def send_email(to_email: str, subject: str, body: str, template: str = "generic"):
if settings.mock_email or not settings.ses_configured:
print(f"\n[MOCK EMAIL] to={to_email} subject={subject}\n{body}\n")
await _log(to_email, subject, template, status="mocked")
return True
try:
import boto3
client = boto3.client(
"ses", region_name=settings.aws_region,
aws_access_key_id=settings.aws_access_key_id,
aws_secret_access_key=settings.aws_secret_access_key,
)
client.send_email(
Source=settings.ses_from_email,
Destination={"ToAddresses": [to_email]},
Message={"Subject": {"Data": subject},
"Body": {"Html": {"Data": body}}},
)
await _log(to_email, subject, template)
return True
except Exception as e: # noqa
await _log(to_email, subject, template, status="failed", error=str(e))
return False
async def send_verification_email(to_email: str, name: str, token: str, code: str = ""):
link = f"{settings.frontend_url}/verify-email?token={token}"
body = (f"Hi {name},\n\n"
f"Your VerifyPack verification code is: {code}\n\n"
f"Or click this link to verify: {link}\n\n"
f"This code expires in 30 minutes.")
await send_email(to_email, "Verify your VerifyPack email", body, "verify_email")
async def send_reset_email(to_email: str, name: str, token: str):
link = f"{settings.frontend_url}/reset-password?token={token}"
await send_email(to_email, "Reset your VerifyPack password",
f"Hi {name}, reset your password: {link}", "reset_password")
async def send_invite_email(to_email: str, org_name: str, token: str):
link = f"{settings.frontend_url}/signup?invite={token}"
await send_email(to_email, f"You're invited to {org_name} on VerifyPack",
f"Join {org_name}: {link}", "invite")

View File

@@ -0,0 +1,102 @@
"""Server-side GST invoice PDF (reportlab). INR, 18% GST."""
import io
from datetime import datetime
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
def _inr(paise_or_rupees: int) -> str:
return f"Rs. {paise_or_rupees:,.2f}"
def build_invoice_pdf(invoice: dict, org: dict) -> bytes:
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=A4)
w, h = A4
y = h - 25 * mm
# Header
c.setFillColor(colors.HexColor("#0b1f4d"))
c.setFont("Helvetica-Bold", 20)
c.drawString(20 * mm, y, "VerifyPack")
c.setFont("Helvetica", 9)
c.setFillColor(colors.grey)
c.drawString(20 * mm, y - 6 * mm, "Tax Invoice")
c.setFont("Helvetica-Bold", 12)
c.setFillColor(colors.black)
c.drawRightString(w - 20 * mm, y, f"Invoice {invoice.get('invoice_number', '')}")
c.setFont("Helvetica", 9)
c.setFillColor(colors.grey)
date = invoice.get("created_at")
date_str = date.strftime("%d %b %Y") if isinstance(date, datetime) else str(date or "")
c.drawRightString(w - 20 * mm, y - 6 * mm, f"Date: {date_str}")
y -= 22 * mm
c.setStrokeColor(colors.HexColor("#e2e8f0"))
c.line(20 * mm, y, w - 20 * mm, y)
# Billed to
y -= 10 * mm
c.setFillColor(colors.black)
c.setFont("Helvetica-Bold", 10)
c.drawString(20 * mm, y, "Billed To")
c.setFont("Helvetica", 9)
c.drawString(20 * mm, y - 5 * mm, org.get("name", ""))
if org.get("gstin"):
c.drawString(20 * mm, y - 10 * mm, f"GSTIN: {org['gstin']}")
if org.get("address"):
c.drawString(20 * mm, y - 15 * mm, org["address"][:60])
c.setFont("Helvetica-Bold", 10)
c.drawRightString(w - 20 * mm, y, "From")
c.setFont("Helvetica", 9)
c.drawRightString(w - 20 * mm, y - 5 * mm, "VerifyPack Technologies")
c.drawRightString(w - 20 * mm, y - 10 * mm, "Bengaluru, Karnataka, India")
# Line items table
y -= 30 * mm
c.setFillColor(colors.HexColor("#f1f5f9"))
c.rect(20 * mm, y, w - 40 * mm, 8 * mm, fill=1, stroke=0)
c.setFillColor(colors.HexColor("#334155"))
c.setFont("Helvetica-Bold", 9)
c.drawString(23 * mm, y + 2.5 * mm, "Description")
c.drawRightString(w - 23 * mm, y + 2.5 * mm, "Amount")
subtotal = invoice.get("subtotal", 0)
gst = invoice.get("gst_amount", 0)
total = invoice.get("total", 0)
y -= 10 * mm
c.setFillColor(colors.black)
c.setFont("Helvetica", 9)
period = invoice.get("period", "")
c.drawString(23 * mm, y, f"{invoice.get('plan_name', 'Plan')} subscription {period}".strip())
c.drawRightString(w - 23 * mm, y, _inr(subtotal))
# Totals
y -= 14 * mm
c.line(w - 80 * mm, y + 6 * mm, w - 20 * mm, y + 6 * mm)
c.setFont("Helvetica", 9)
c.drawRightString(w - 55 * mm, y, "Subtotal")
c.drawRightString(w - 23 * mm, y, _inr(subtotal))
y -= 6 * mm
c.drawRightString(w - 55 * mm, y, "GST (18%)")
c.drawRightString(w - 23 * mm, y, _inr(gst))
y -= 8 * mm
c.setFont("Helvetica-Bold", 11)
c.drawRightString(w - 55 * mm, y, "Total")
c.drawRightString(w - 23 * mm, y, _inr(total))
# Footer
c.setFont("Helvetica", 8)
c.setFillColor(colors.grey)
c.drawString(20 * mm, 20 * mm, f"Place of supply: {invoice.get('place_of_supply', 'Karnataka')}")
c.drawString(20 * mm, 15 * mm, "This is a computer-generated invoice and does not require a signature.")
c.drawRightString(w - 20 * mm, 15 * mm, f"Status: {str(invoice.get('status', '')).upper()}")
c.showPage()
c.save()
return buf.getvalue()

View File

@@ -0,0 +1,70 @@
"""QR generation engine. No credit deduction — generation is free and unlimited."""
import io
import secrets
import zipfile
import segno
from app.core.config import settings
from app.models.documents import QRStyle
_EC_MAP = {"L": "l", "M": "m", "Q": "q", "H": "h"}
def new_code(prefix: str = "vp") -> str:
return f"{prefix}{secrets.token_urlsafe(8).replace('-', '').replace('_', '')[:10]}".lower()
def verify_url(code: str) -> str:
return f"{settings.frontend_url}/v/{code}"
def _make(code: str, style: QRStyle) -> segno.QRCode:
ec = _EC_MAP.get(style.error_correction.upper(), "m")
if style.logo_url:
ec = "h" # force high error correction when a logo is overlaid
return segno.make(verify_url(code), error=ec)
def render_png(code: str, style: QRStyle, scale: int = 10) -> bytes:
buf = io.BytesIO()
_make(code, style).save(buf, kind="png", scale=scale,
dark=style.fg_color, light=style.bg_color,
border=style.quiet_zone)
return buf.getvalue()
def render_svg(code: str, style: QRStyle, scale: int = 10) -> bytes:
buf = io.BytesIO()
_make(code, style).save(buf, kind="svg", scale=scale,
dark=style.fg_color, light=style.bg_color,
border=style.quiet_zone)
return buf.getvalue()
def render_pdf(code: str, style: QRStyle, scale: int = 10) -> bytes:
buf = io.BytesIO()
_make(code, style).save(buf, kind="pdf", scale=scale,
dark=style.fg_color, light=style.bg_color,
border=style.quiet_zone)
return buf.getvalue()
def render(code: str, style: QRStyle, fmt: str = "png") -> tuple[bytes, str]:
fmt = fmt.lower()
if fmt == "svg":
return render_svg(code, style), "image/svg+xml"
if fmt == "pdf":
return render_pdf(code, style), "application/pdf"
return render_png(code, style), "image/png"
def zip_codes(codes: list[str], style: QRStyle, fmt: str = "png") -> bytes:
"""Bundle per-pack QRs (Option 2) into a single downloadable ZIP."""
buf = io.BytesIO()
ext = {"svg": "svg", "pdf": "pdf"}.get(fmt.lower(), "png")
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
for i, code in enumerate(codes, start=1):
data, _ = render(code, style, fmt)
zf.writestr(f"qr_{i:05d}_{code}.{ext}", data)
return buf.getvalue()

View File

@@ -0,0 +1,55 @@
"""Razorpay integration with a mock fallback for local dev.
When RAZORPAY_KEY_ID/SECRET are unset (or MOCK), create_order returns a fake
order and verify_signature always succeeds, so the whole billing flow can be
exercised end-to-end without real keys.
"""
import hashlib
import hmac
import secrets
from typing import Optional
from app.core.config import settings
def is_live() -> bool:
return settings.razorpay_configured
def create_order(amount_paise: int, receipt: str, notes: Optional[dict] = None) -> dict:
if not is_live():
return {
"id": f"order_mock_{secrets.token_hex(8)}",
"amount": amount_paise,
"currency": "INR",
"receipt": receipt,
"status": "created",
"mock": True,
}
import razorpay
client = razorpay.Client(auth=(settings.razorpay_key_id, settings.razorpay_key_secret))
return client.order.create({
"amount": amount_paise,
"currency": "INR",
"receipt": receipt,
"notes": notes or {},
})
def verify_payment_signature(order_id: str, payment_id: str, signature: str) -> bool:
if not is_live():
return True # mock: accept any payment
body = f"{order_id}|{payment_id}"
expected = hmac.new(
settings.razorpay_key_secret.encode(), body.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def verify_webhook_signature(payload: bytes, signature: str) -> bool:
if not settings.razorpay_webhook_secret:
return not is_live() # accept in mock mode only
expected = hmac.new(
settings.razorpay_webhook_secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

View File

@@ -0,0 +1,46 @@
"""S3 uploads with presigned URLs, plus a local-filesystem mock."""
import os
import uuid
from app.core.config import settings
# Must match the StaticFiles mount in main.py (app/../_uploads = backend/app/_uploads)
LOCAL_DIR = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"..",
"..",
"_uploads",
)
)
async def upload_file(content: bytes, filename: str, content_type: str = "application/octet-stream") -> str:
key = f"{uuid.uuid4().hex}_{filename}"
if settings.mock_storage or not (settings.aws_access_key_id and settings.aws_secret_access_key):
os.makedirs(LOCAL_DIR, exist_ok=True)
path = os.path.join(LOCAL_DIR, key)
with open(path, "wb") as f:
f.write(content)
# served by FastAPI static mount at /uploads
return f"/uploads/{key}"
import boto3
client = boto3.client(
"s3", region_name=settings.aws_region,
aws_access_key_id=settings.aws_access_key_id,
aws_secret_access_key=settings.aws_secret_access_key,
)
client.put_object(Bucket=settings.s3_bucket, Key=key, Body=content, ContentType=content_type)
return presigned_url(key)
def presigned_url(key: str, expires: int = 3600) -> str:
import boto3
client = boto3.client(
"s3", region_name=settings.aws_region,
aws_access_key_id=settings.aws_access_key_id,
aws_secret_access_key=settings.aws_secret_access_key,
)
return client.generate_presigned_url(
"get_object", Params={"Bucket": settings.s3_bucket, "Key": key}, ExpiresIn=expires
)