Service Factories (Email, Payment, Storage, Cache) - Deep Dive Documentation¶
Generated: 2026-03-29
Scope: saas-boilerplate/src/lib/{email,payment,storage,cache}/ + config + helper consumers
Files Analyzed: 45+ (20 lib factory files + 16 config/consumer files + helper layer)
Lines of Code: ~1,400+ (factory core) + ~800+ (helper/config layer)
Workflow Mode: Exhaustive Deep-Dive
Issues Found: ~147 unique (28 Critical, 33 High, 46 Medium, 40 Low) + 6 systemic
Overview¶
Four factory-pattern subsystems in src/lib/ provide abstracted interfaces for external service integration: email delivery (3 providers), payment processing (Stripe + LemonSqueezy), file storage (AWS S3, Cloudinary, Local), and caching (node-cache). Each follows a Strategy Pattern with interface + concrete implementations + factory selector.
The central discovery: These factories represent an incomplete refactoring. Only ~11% of consumers use the factory abstractions; ~89% use legacy helper/config systems that predate them. Two entire subsystems (storage, cache) are 100% dead code with zero consumers.
Purpose: Decouple business logic from third-party service providers via the Strategy Pattern Key Responsibilities: Provider selection, configuration, SDK initialization, request/response normalization Integration Points: SMTP/Resend/Brevo APIs, Stripe/LemonSqueezy SDKs, AWS S3/Cloudinary/Local filesystem, node-cache in-memory store
Factory Adoption & Production Readiness¶
| Factory | Consumers Using Factory | Consumers Using Legacy | Adoption % | Production Ready |
|---|---|---|---|---|
| 2 | 4 | 33% | ~40% (broken by default) | |
| Payment | 1 | ~10 | ~9% | ~15% (hardcoded "stripe") |
| Storage | 0 | 6 | 0% | 0% (entire dir dead code) |
| Cache | 0 | 5 | 0% | 0% (entire dir dead code) |
Competing Systems Inventory¶
| Subsystem | # Systems | Implementations |
|---|---|---|
| 3 | lib/email/EmailFactory, helper/email.ts (legacy MAIL_TYPE), lib/email/email.ts (dead) |
|
| Payment | 4 | lib/payment/PaymentFactory, config/stripe.ts (sync), config/getStripe.ts (async+DB), config/getLemonsqueezy.ts |
| Storage | 3 | lib/storage/StorageFactory (dead), helper/handleFileUpload.ts chain, config/cloudinary.ts |
| Cache | 3 | lib/cache/Node-cache.ts (dead), shared/cache.ts (production), shared/permissionCache.ts (AsyncLocalStorage) |
| Total | 13 |
Complete File Inventory¶
Email Factory (lib/email/ — 9 files, ~157 LOC)¶
src/lib/email/IEmailService.ts¶
Purpose: Defines the contract interface that all email providers must implement. Single method sendEmail with IEmailSendPayload.
Lines of Code: ~8
File Type: Interface definition
What Future Contributors Must Know: This is the abstraction layer — any new email provider must implement this interface. However, the interface is minimal (no batch, CC/BCC, attachments, or error contract).
Exports:
- interface IEmailService { sendEmail(payload: IEmailSendPayload): Promise<void> } — Provider contract
- interface IEmailSendPayload { to: string; subject: string; html: string } — Email payload type
Dependencies: - None (standalone interface)
Used By:
- EmailFactory.ts (return type)
- NodeMailerService.ts (implements)
- ResendService.ts (implements)
- BrevoService.ts (implements — but missing explicit implements keyword)
Key Implementation Details:
- to is string only — no array support for multiple recipients
- No from field — each provider handles sender differently
- No attachment support (helper version has it)
- No error contract — callers cannot distinguish failure modes
Patterns Used: - Strategy Pattern: Interface for dependency inversion
Side Effects: None (pure type definition)
Error Handling: No error contract defined — each provider handles errors differently
Testing: No test files exist - Coverage: 0%
src/lib/email/EmailFactory.ts¶
Purpose: Factory that selects an email provider based on EMAIL_SERVICE env var. Returns singleton instances of NodeMailer, Resend, or Brevo services.
Lines of Code: ~28
File Type: Factory / Provider selector
What Future Contributors Must Know: This factory is broken by default — EMAIL_SERVICE is not in .env or .env.example, so it always throws "Unknown email service type: undefined". Only 2 consumers use this (registration + invite handlers), and both silently fail.
Exports:
- class EmailFactory { static getEmailService(): IEmailService } — Singleton factory method
Dependencies:
- IEmailService from ./IEmailService
- NodeMailerService from ./NodeMailerService
- ResendService from ./ResendService
- BrevoService from ./BrevoService
- config from ../../config (reads config.mail.service)
Used By:
- app/events/handlers/registerEmailHandler.ts — Registration verification emails
- app/events/handlers/inviteUserCreate.handler.ts — Invite emails
Key Implementation Details:
static getEmailService(): IEmailService {
const emailServiceType = config.mail.service; // EMAIL_SERVICE env var
console.log("Email service type:", emailServiceType); // Logs on every call
switch (emailServiceType) {
case "nodemailer": return new NodeMailerService();
case "resend": return new ResendService();
case "brevo": return new BrevoService();
default: throw new Error(`Unknown email service type: ${emailServiceType}`);
}
}
Side Effects:
- console.log on every factory call (leaks config in production)
- Reads config from environment
Error Handling: Throws on unknown/undefined provider — crashes caller
Issues:
- F1 [CRITICAL]: EMAIL_SERVICE missing from .env and .env.example — factory always throws
- F2 [MEDIUM]: console.log on every call
- F3 [LOW]: No runtime type narrowing — arbitrary strings passed through
- Comment says "storage service" — copy-pasted from StorageFactory
src/lib/email/NodeMailerService.ts¶
Purpose: SMTP email provider using Nodemailer. Creates a transporter, verifies SMTP connection, then sends email. Lines of Code: ~35 File Type: Service implementation
What Future Contributors Must Know: Uses explicit new Promise(async ...) anti-pattern that can cause unhandled rejections. Verifies SMTP on every send (latency hit). Hardcodes from address with "Sass" typo and example.com domain. Disables TLS verification (rejectUnauthorized: false).
Exports:
- class NodeMailerService implements IEmailService { sendEmail(payload): Promise<void> } — SMTP sender
Dependencies:
- nodemailer — SMTP library
- IEmailService, IEmailSendPayload from ./IEmailService
- config from ../../config
Used By:
- EmailFactory.ts (instantiated when EMAIL_SERVICE=nodemailer)
Key Implementation Details:
- Creates new transporter per send (no connection pooling)
- Calls transporter.verify() before every send — adds latency
- Hardcoded from: "Sass Template" <[email protected]> (typo + placeholder domain)
- rejectUnauthorized: false disables TLS cert verification
Side Effects:
- SMTP connection attempt on every send
- console.log("Email sent successfully") on success
Issues:
- N1 [HIGH]: rejectUnauthorized: false enables MITM attacks
- N2 [HIGH]: new Promise(async (resolve, reject) => ...) anti-pattern
- N3 [MEDIUM]: Hardcoded from address ("Sass" typo, example.com)
- N4 [LOW]: transPorter inconsistent casing
- N5 [LOW]: verify() on every send adds latency
src/lib/email/ResendService.ts¶
Purpose: Email provider using Resend HTTP API. Sends transactional emails via Resend SDK. Lines of Code: ~18 File Type: Service implementation
What Future Contributors Must Know: Zero error handling — all Resend SDK errors propagate as unhandled. Uses SMTP user email (MAIL_USER) as from address instead of Resend verified domain. Unsafe as string cast on config.
Exports:
- class ResendService implements IEmailService { sendEmail(payload): Promise<void> } — Resend API sender
Dependencies:
- resend — Resend SDK
- IEmailService, IEmailSendPayload from ./IEmailService
- config from ../../config
Used By:
- EmailFactory.ts (instantiated when EMAIL_SERVICE=resend)
Key Implementation Details:
const resend = new Resend(config.mail.pass as string); // RESEND_API_KEY via MAIL_PASS
await resend.emails.send({
from: config.mail.user as string, // SMTP user, not Resend domain
to: payload.to,
subject: payload.subject,
html: payload.html,
});
Issues:
- R1 [HIGH]: Unsafe as string cast — sends undefined if MAIL_USER unset
- R2 [MEDIUM]: Zero error handling
- R3 [MEDIUM]: Wrong from address source (SMTP user instead of Resend verified domain)
- R4 [LOW]: Redundant html spread
- R5 [LOW]: No attachment support (helper version has it)
src/lib/email/BrevoService.ts¶
Purpose: Email provider using Brevo (formerly Sendinblue) API. Sends transactional emails via Brevo SDK. Lines of Code: ~45 File Type: Service implementation
What Future Contributors Must Know: Silently swallows all errors — catch block logs but doesn't rethrow. sendEmail() resolves even on failure, so callers believe the email was sent. Contains hardcoded test email object that is never used. Sets API key on every send instead of constructor.
Exports:
- class BrevoService implements IEmailService { sendEmail(payload): Promise<void> } — Brevo API sender
Dependencies:
- @getbrevo/brevo — Brevo SDK
- IEmailService, IEmailSendPayload from ./IEmailService
- config from ../../config
Used By:
- EmailFactory.ts (instantiated when EMAIL_SERVICE=brevo)
Key Implementation Details:
async sendEmail(payload: IEmailSendPayload): Promise<void> {
try {
apiInstance.setApiKey(/*...*/); // Re-sets on every call
const sendSmtpEmail = new brevo.SendSmtpEmail();
// ... configure email ...
await apiInstance.sendTransacEmail(sendSmtpEmail);
} catch (error) {
console.error("Error sending email:", error); // Logs but does NOT rethrow
// Caller thinks email was sent successfully
}
}
Issues:
- B1 [CRITICAL]: Silent error swallowing — sendEmail resolves on failure
- B2 [HIGH]: Hardcoded test email object (lines 25-33) with "Hello from Brevo SDK" — dead code
- B3 [MEDIUM]: Unused imports (CreateContact, ContactsApi)
- B4 [MEDIUM]: setApiKey on every send instead of constructor
- B5 [HIGH]: Unsafe as string cast on config values
- B6 [LOW]: Hardcoded sender name "Your App" in dead test object
- B7 [LOW]: BREVO_API_KEY absent from all env files
src/lib/email/email.ts — DEAD CODE¶
Purpose: Alternative email sender function with 'use server' directive (React Server Actions — meaningless in Express.js backend).
Lines of Code: ~33
File Type: Dead standalone function
What Future Contributors Must Know: This file has ZERO importers. Contains a Next.js 'use server' directive in an Express.js backend. Creates its own nodemailer transporter separate from NodeMailerService. Hardcodes "Sass" from address with example.com domain. In dev mode, silently returns success without sending.
Exports:
- async function sendEmail(payload: IEmailSendPayload): Promise<void> — Standalone email sender
Dependencies:
- nodemailer
- Local IEmailSendPayload interface (duplicated, not imported from IEmailService.ts)
- config
Used By: Nothing — DEAD CODE
Issues:
- E1 [HIGH]: 'use server' directive in Express.js backend
- E2 [HIGH]: Dev mode silently swallows failures (returns success)
- E3 [MEDIUM]: Duplicates IEmailSendPayload locally
- E4 [MEDIUM]: Hardcoded "Sass" typo and example.com
- E5 [LOW]: verify() on every send
src/lib/email/transporterMail.ts — DEAD CODE¶
Purpose: Creates and exports a nodemailer transporter instance. Lines of Code: ~18 File Type: Dead configuration module
What Future Contributors Must Know: Only imported by dead email.ts — transitively dead. Creates a separate transporter from NodeMailerService with rejectUnauthorized: false. Contains commented-out CommonJS export.
Exports:
- const transporter — Nodemailer transporter instance
Dependencies:
- nodemailer
- config
Used By: Only lib/email/email.ts (which is also dead)
Issues:
- TM1 [HIGH]: rejectUnauthorized: false (MITM risk — same as NodeMailerService)
- TM2 [MEDIUM]: Duplicate transporter creation (separate from NodeMailerService)
- TM3 [LOW]: Dead CommonJS export comment // module.exports = transporter
- TM4 [LOW]: Transitively dead — only consumer is dead email.ts
src/lib/email/template/signupEmailVerification.ts — DEAD CODE¶
Purpose: Generates an HTML email template for signup email verification with an embedded verification link. Lines of Code: ~13 File Type: Dead template function
What Future Contributors Must Know: Zero importers anywhere in codebase. All email templates used in production are in src/template/ (different directory). Contains a pointless ${"there"} template literal expression and interpolates verifyUrl without URL escaping (XSS risk if URL contains user input).
Exports:
- function signupEmailVerification(verifyUrl: string): string — HTML template generator
Dependencies: None
Used By: Nothing — DEAD CODE
Issues:
- ST1 [CRITICAL]: Zero importers — completely dead code
- ST2 [MEDIUM]: XSS risk — verifyUrl not escaped
- ST3 [LOW]: ${"there"} pointless template expression
- ST4 [LOW]: Only 1 template in folder (no reset, OTP, invite templates)
Payment Factory (lib/payment/ — 4 files, ~180 LOC)¶
src/lib/payment/payment.type.ts¶
Purpose: Type definitions for the payment abstraction layer. Defines interfaces for payment options, verification results, and the payment service contract. Lines of Code: ~25 File Type: Type definitions
What Future Contributors Must Know: The IPaymentService interface is extremely thin — only createCheckout, verify, and createBillingPortal. No refund, cancel, customer management, or subscription operations. The amount field in IPaymentOptions is never consumed by any implementation.
Exports:
- interface IPaymentOptions { planId: string | number; customerId: string; amount: number; currency: string; metadata: Record<string, unknown> } — Checkout options
- interface IPaymentVerifyResult { success: boolean; data: any } — Verification result
- interface IPaymentService { createCheckout(options): Promise<{checkoutUrl, sessionId}>; verify(sessionId): Promise<IPaymentVerifyResult>; createBillingPortal(customerId): Promise<{url}> } — Provider contract
- type PaymentProvider = "stripe" | "lemonsqueezy" — Provider union type
Dependencies: None
Used By:
- PaymentFactory.ts, StripeService.ts, paymentService.ts
Issues:
- #1 [MEDIUM]: amount field never consumed by any implementation
- #2 [LOW]: planId: string | number — number branch misleading for Stripe
- #3 [LOW]: metadata: Record<string, unknown> — Stripe expects Record<string, string>
- #4 [MEDIUM]: Interface too thin — no refund, cancel, customer, or validate methods
- #5 [LOW]: verify returns any for data — eliminates type safety
src/lib/payment/PaymentFactory.ts¶
Purpose: Factory that creates payment service instances based on provider name. Currently only supports Stripe despite accepting "lemonsqueezy" in type. Lines of Code: ~18 File Type: Factory / Provider selector
What Future Contributors Must Know: Accepts "lemonsqueezy" in the TypeScript type but throws at runtime because there's no case for it. No singleton/caching — creates new StripeService per call. Only 1 consumer (paymentService.ts) and it hardcodes "stripe" anyway.
Exports:
- class PaymentFactory { static create(provider: PaymentProvider): IPaymentService } — Factory method
Dependencies:
- StripeService from ./StripeService
- PaymentProvider from ./payment.type
Used By:
- app/modules/v1/payment.service.ts (hardcodes PaymentFactory.create("stripe"))
Key Implementation Details:
static create(provider: PaymentProvider): IPaymentService {
switch (provider) {
case "stripe": return new StripeService();
// NO case for "lemonsqueezy" — throws default
default: throw new Error("Unsupported payment provider");
}
}
Issues:
- #6 [CRITICAL]: "lemonsqueezy" accepted by TypeScript but throws at runtime
- #7 [HIGH]: No singleton — new StripeService per call
- #8 [MEDIUM]: Factory doesn't read provider from config — caller must hardcode
src/lib/payment/StripeService.ts¶
Purpose: Stripe implementation of IPaymentService. Creates checkout sessions, verifies payments, and manages billing portal.
Lines of Code: ~108
File Type: Service implementation
What Future Contributors Must Know: Uses stripe?. optional chaining that returns undefined instead of throwing when Stripe is null. The cancelUrl uses {CHECKOUT_SESSION_ID} template — Stripe only replaces this in success_url, not cancel_url, so cancel redirects to a 404. Has methods (createCheckoutSession, isValidPriceId) that leak outside the IPaymentService interface.
Exports:
- class StripeService implements IPaymentService — Stripe payment provider
- createCheckout(options: IPaymentOptions): Promise<{checkoutUrl, sessionId}>
- verify(sessionId: string): Promise<IPaymentVerifyResult>
- createBillingPortal(customerId: string): Promise<{url}>
- createCheckoutSession(priceId, customerId, metadata) — NOT on interface
- isValidPriceId(priceId) — NOT on interface
Dependencies:
- stripe from stripe SDK
- config from ../../config
- Types from ./payment.type
Used By:
- PaymentFactory.ts (instantiated)
Key Implementation Details:
constructor() {
this.stripe = config.stripe.secretKey
? new Stripe(config.stripe.secretKey)
: null; // Null if no key
}
// Then uses this.stripe?.checkout.sessions.create(...)
// Returns { checkoutUrl: undefined, sessionId: undefined } when null!
Side Effects:
- Stripe API calls (checkout session create, session retrieve, billing portal create)
- Reads CLIENT_URL env var for redirect URLs
Issues:
- #9 [CRITICAL]: cancelUrl uses {CHECKOUT_SESSION_ID} — not replaced by Stripe in cancel_url
- #10 [CRITICAL]: Optional chaining returns { checkoutUrl: undefined } silently instead of throwing
- #11 [HIGH]: Methods leak outside IPaymentService interface
- #12 [MEDIUM]: verify() silently returns { success: false } when Stripe is null
- #13 [MEDIUM]: No idempotency keys — retries create duplicate sessions
- #14 [LOW]: products: any[] untyped parameter
src/lib/payment/paymentService.ts¶
Purpose: High-level payment orchestration that uses PaymentFactory to create checkout sessions, verify payments, and manage billing portals. Lines of Code: ~29 File Type: Business logic orchestrator
What Future Contributors Must Know: Hardcodes PaymentFactory.create("stripe") — the paymentGateway parameter passed by callers is completely ignored. This makes the entire multi-provider architecture non-functional.
Exports:
- createPaymentForSubscription(paymentGateway, options): Promise<{checkoutUrl, sessionId}> — Create checkout
- verifyPayment(paymentGateway, sessionId): Promise<IPaymentVerifyResult> — Verify payment
- createBillingPortal(customerId): Promise<{url}> — Create portal
Dependencies:
- PaymentFactory from ./PaymentFactory
- Types from ./payment.type
Used By:
- app/modules/v1/Payment/payment.controller.ts
Issues:
- #32 [CRITICAL]: paymentGateway parameter completely ignored — hardcoded "stripe"
- #33 [HIGH]: Dead createStripePaymentIntent function (33 LOC, references "paypal")
- #34/#35 [HIGH]: Dead imports trigger SDK evaluation
Payment-Related Config & Consumer Files¶
config/stripe.ts (Sync Stripe init)¶
- Creates Stripe instance synchronously from
STRIPE_SECRETenv var - Still imported by 5 files despite being superseded by
getStripe.ts - Issue #15 [HIGH]: Dual Stripe initialization paths
config/getStripe.ts (Async Stripe init with DB fallback)¶
- Creates Stripe from env var, falls back to AppConfig DB (AES-256-GCM decrypted)
- Only factory with encrypted DB secret support
- Issue #17 [HIGH]: Circular dependency with
stripe.service.ts - Issue #18 [HIGH]: No cache invalidation — requires process restart after key rotation
- Issue #19 [MEDIUM]: Decryption crash not caught
config/getLemonsqueezy.ts (LemonSqueezy init)¶
- Issue #21 [CRITICAL]: Hardcoded
localhost:5500redirect URL - Issue #22 [CRITICAL]:
user_idset to email instead of userId - Issue #39 [CRITICAL]: Hardcoded webhook secret
"c3b451346788c32"in source
modules/v1/stripe.service.ts (Business Stripe ops — naming collision)¶
- 261 LOC, 5 consumers — the "real" Stripe service used by most of the codebase
- Exports object literal (not class) — different from
lib/payment/StripeService.ts(class) - Issue #26 [CRITICAL]: References
stripeProductIdcolumn that doesn't exist on Package entity - Issue #27 [HIGH]:
isDeletedfilter on entity using@DeleteDateColumn(different mechanism) - Issue #28 [HIGH]: Billing portal config created on every call
Storage Factory (lib/storage/ — 5 files, ~250 LOC) — 100% DEAD CODE¶
src/lib/storage/IStorageService.ts¶
Purpose: Interface contract for storage providers. Defines upload, delete, commit, and rollback methods. Lines of Code: ~10 File Type: Interface definition — DEAD CODE
What Future Contributors Must Know: Zero consumers. The commit() and rollback() methods are never implemented by any provider. The key field is optional in upload return but required for delete().
Exports:
- interface IStorageService { upload(file): Promise<{url, key?}>; delete(key): Promise<void>; commit?(): Promise<void>; rollback?(): Promise<void> } — Storage contract
Issues:
- #1 [MEDIUM]: key optional in return, required for delete()
- #2 [LOW]: commit()/rollback() never implemented
- #3 [LOW]: No download/exists methods
src/lib/storage/StorageFactory.ts¶
Purpose: Factory that selects storage provider based on IMAGE_STORAGE config. Returns singleton instance.
Lines of Code: ~27
File Type: Factory — DEAD CODE
What Future Contributors Must Know: Zero consumers anywhere. Crashes on the default config value "local" because there is no case "local" in the switch. Only handles "aws" and "cloudinary". Does not import LocalSotrage.ts at all.
Exports:
- function getImageStorageService(): IStorageService — Factory function (not class-based like other factories)
Issues:
- #4 [CRITICAL]: Default config "local" crashes — no case in switch
- #5 [CRITICAL]: Zero consumers — entire factory unused
- #6 [HIGH]: No LocalSotrage import — cannot add local support
- #7 [MEDIUM]: Singleton not resettable (no way to change provider in tests)
- #8 [LOW]: Function name getImageStorageService vs file name StorageFactory
src/lib/storage/AwsStorage.ts¶
Purpose: AWS S3 storage provider using @aws-sdk/client-s3.
Lines of Code: ~89
File Type: Service implementation — DEAD CODE
What Future Contributors Must Know: Pointless base64 round-trip: converts buffer to base64 string then back to buffer before uploading. No filename sanitization — special characters in S3 keys. Public URL assumes bucket is public (no ACL set). No error handling or retry logic.
Exports:
- class AwsStorage implements IStorageService — S3 upload/delete
Issues: - #9 [HIGH]: Pointless buffer→base64→buffer round-trip - #10 [HIGH]: Mixed config sources (constructor vs process.env) - #11 [HIGH]: No filename sanitization - #12 [MEDIUM]: No file size/type validation - #13 [MEDIUM]: URL assumes public bucket - #14 [MEDIUM]: No error handling/retry - #15 [MEDIUM]: No concurrency limit on batch upload - #16 [LOW]: Key collision risk (Date.now() millisecond resolution) - #17 [LOW]: No content disposition header
src/lib/storage/CloudinaryStorage.ts¶
Purpose: Cloudinary storage provider using Cloudinary SDK's upload stream. Lines of Code: ~74 File Type: Service implementation — DEAD CODE
What Future Contributors Must Know: Missing explicit implements IStorageService — no compile-time contract enforcement. Sends base64 string through upload_stream instead of binary buffer, making files ~33% larger. delete() ignores Cloudinary response.
Exports:
- class CloudinaryStorage — Cloudinary upload/delete (missing implements IStorageService)
Issues:
- #18 [CRITICAL]: Missing implements IStorageService
- #19 [HIGH]: Base64 string to upload_stream instead of buffer
- #20 [MEDIUM]: Hardcoded "uploads" folder
- #21 [MEDIUM]: delete() ignores result
- #22 [MEDIUM]: No file type validation
- #23 [LOW]: Unnecessary non-null assertions
- #24 [LOW]: No concurrency limit on batch
src/lib/storage/LocalSotrage.ts (Filename Typo)¶
Purpose: Local filesystem storage provider. Writes uploaded files to local disk. Lines of Code: ~50 File Type: Service implementation — DEAD CODE
What Future Contributors Must Know: Filename typo "Sotrage" — never imported anywhere (not even by the factory). upload() writes base64 TEXT to disk instead of binary — every saved file is corrupted/unusable. Path traversal vulnerability via unsanitized file.originalname. saveAfterUpload() has swapped arguments (folderName passed as fileName).
Exports:
- class LocalStorage implements IStorageService — Local filesystem upload/delete
Issues:
- #25 [CRITICAL]: Filename typo "LocalSotrage.ts"
- #26 [CRITICAL]: Writes base64 text, not binary — files corrupted
- #27 [CRITICAL]: Path traversal via unsanitized file.originalname
- #28 [HIGH]: saveAfterUploads() ignores fileName param
- #29 [CRITICAL]: saveAfterUpload() swaps arguments
- #30 [MEDIUM]: No directory creation before write
- #31 [MEDIUM]: fs.unlinkSync blocks event loop in async method
- #32 [MEDIUM]: No overwrite protection
- #33 [LOW]: saveAfterUpload/saveAfterUploads not on interface
- #34 [LOW]: Partial fs usage
Actual Storage System (Helper Layer — In Production)¶
The real storage system uses helper/handleFileUpload.ts → helper/uploadImage.ts → helper/uploadToCloudinary.ts / helper/cloud/s3.ts.
Consumers (5 modules): Blog, User, Setting, GenericPage, Image
Key Issues in Helper Layer:
- #35 [CRITICAL]: Two independent upload systems share NO code
- #36 [HIGH]: handleFileUpload in "local" mode returns URL but does NOT write file
- #37 [HIGH]: Three different S3 implementations with different key formats
- #38 [HIGH]: Inconsistent storage type names ("s3" in helper vs "aws" in factory)
- #39 [MEDIUM]: Force-converts all uploads to WebP (non-image files broken)
- #40 [HIGH]: Hardcoded image/jpeg MIME type in uploadToCloudinary
- #41 [MEDIUM]: uploadImage only supports Cloudinary despite generic name
- #42 [MEDIUM]: Dead imports in image.service.ts
Cache (lib/cache/ — 2 files, ~37 LOC) — 100% DEAD CODE¶
src/lib/cache/cache.interface.ts¶
Purpose: Interface for cache operations. Defines get/set/del/delByPrefix/has/flush contract. Lines of Code: ~5 File Type: Interface definition — DEAD CODE
Exports:
- interface ICache { get<T>(key): Promise<T|undefined>; set(key, value, ttl?): Promise<void>; del(key): Promise<void>; delByPrefix(prefix): Promise<void>; has(key): Promise<boolean>; flush(): Promise<void> } — Cache contract
Issues:
- H1 [HIGH]: delByPrefix() is most-used method in production cache but NOT on this interface
- L10 [LOW]: All methods async for synchronous node-cache ops (over-engineering for Redis swap)
src/lib/cache/Node-cache.ts¶
Purpose: Class-based node-cache implementation following the ICache interface. Abandoned migration from the functional shared/cache.ts.
Lines of Code: ~32
File Type: Service implementation — DEAD CODE
What Future Contributors Must Know: Zero consumers. A git branch cache-v1.0.0 suggests this was an abandoned migration. Uses useClones: false (unlike production shared/cache.ts which uses default true) — allows cache mutation bugs.
Exports:
- class NodeCacheService implements ICache — OOP cache wrapper
Issues:
- C1 [CRITICAL]: Entirely dead code — zero consumers
- C3 [CRITICAL]: Different useClones behavior than production cache
- C5 [CRITICAL]: Dual cache infrastructure with no migration path
Production Cache System (shared/cache.ts)¶
Lines of Code: ~85 Used By: Role service (8 ops), MenuItem service (9 ops), Analytic service (import only — dead)
Key Issues:
- C2 [CRITICAL]: Comment says "default TTL: 5 minutes" but code sets 3600s (1 hour)
- C4 [CRITICAL]: TTL 60 * 24 = 1440s (24 min) with comments saying "cache for 1 day" — off by 60x
- H2 [HIGH]: No maxKeys limit — unbounded memory growth
- H3 [HIGH]: JSON double-serialization (stringify + useClones deep-clone)
- H4 [HIGH]: No cross-request permission caching — every auth request hits DB
- H5 [HIGH]: Wrong file path in header comment
- H6 [HIGH]: useClones not explicitly set — relies on library default
- M2 [MEDIUM]: revalidateCache is non-atomic (del+set brief miss window)
- M3 [MEDIUM]: addPermissionToCache allows runtime injection
- M7 [MEDIUM]: No cache warming strategy — cold-start penalty
- L1 [LOW]: validateCacheOwnership returns true for "no cache" — misleading
- L4 [LOW]: set() uses if (ttlSeconds) — falsy for TTL of 0
Contributor Checklist¶
Risks & Gotchas¶
- Email registration is silently broken in default configuration (EMAIL_SERVICE unset)
- Real SMTP credentials are committed in
.env(Gmail app password) - Brevo never reports failures — emails silently drop
- Payment always uses Stripe regardless of
paymentGatewayparam - LemonSqueezy webhook secret is hardcoded in source code
- Entire lib/storage/ and lib/cache/ are dead code — do not try to "fix" them without migration plan
- cancelUrl in Stripe is broken — cancel redirects to 404
- 3 separate S3 implementations with different behaviors
- TTL comments are lies — 5 min says 1 hour, 1 day means 24 minutes
Pre-change Verification Steps¶
- Check which email system a handler uses before modifying (factory vs helper)
- Search for
EMAIL_SERVICEandMAIL_TYPEto understand which system is active - Verify Stripe SDK calls against latest Stripe docs (SDK version pinned)
- Test cancel URL behavior with actual Stripe checkout
- Check
IMAGE_STORAGEvalue before assuming which upload path runs - Verify cache TTLs match documented behavior (read code, not comments)
Suggested Tests Before PR¶
- Unit: Each email provider send (mock SMTP/API, verify payload shape)
- Unit: EmailFactory provider selection (each case + unknown + undefined)
- Unit: PaymentFactory with mock Stripe (checkout, verify, portal)
- Unit: StripeService null-Stripe path (should throw, not return undefined)
- Integration: End-to-end registration email (factory → provider → delivery)
- Integration: Stripe checkout flow (create → redirect → verify → record)
- Unit: Cache TTL behavior (set with TTL, verify expiry timing)
- Unit: Cache key collision test (concurrent sets with Date.now keys)
Architecture & Design Patterns¶
Code Organization¶
src/lib/
├── email/ # 3 providers (NodeMailer, Resend, Brevo)
│ ├── IEmailService.ts # Interface
│ ├── EmailFactory.ts # Factory (broken by default)
│ ├── NodeMailerService.ts # SMTP provider
│ ├── ResendService.ts # HTTP API provider
│ ├── BrevoService.ts # HTTP API provider (silent errors)
│ ├── email.ts # DEAD CODE
│ ├── transporterMail.ts # DEAD CODE
│ └── template/
│ └── signupEmailVerification.ts # DEAD CODE
├── payment/ # 1 provider (Stripe only)
│ ├── payment.type.ts # Types + interface
│ ├── PaymentFactory.ts # Factory (hardcoded "stripe" by caller)
│ ├── StripeService.ts # Stripe implementation
│ └── paymentService.ts # Business orchestrator
├── storage/ # ENTIRELY DEAD CODE
│ ├── IStorageService.ts # Interface
│ ├── StorageFactory.ts # Factory (crashes on default config)
│ ├── AwsStorage.ts # S3 provider
│ ├── CloudinaryStorage.ts # Cloudinary provider (missing implements)
│ └── LocalSotrage.ts # Local provider (filename typo, writes corrupted files)
└── cache/ # ENTIRELY DEAD CODE
├── cache.interface.ts # Interface
└── Node-cache.ts # node-cache wrapper
Design Patterns¶
- Strategy Pattern: All 4 subsystems define an interface + multiple implementations. However, consistency varies — Email uses class-based factory, Payment uses class-based factory, Storage uses function-based factory, Cache has no factory.
- Singleton Pattern: Email and Storage factories cache instances; Payment does not (new per call).
- Factory Method: EmailFactory and PaymentFactory use static
create/getEmailServicemethods. StorageFactory uses a module-scoped function.
Naming Inconsistencies¶
| Aspect | Payment | Storage | Cache | |
|---|---|---|---|---|
| Interface file | IEmailService.ts |
Inside payment.type.ts |
IStorageService.ts |
cache.interface.ts |
| Factory file | EmailFactory.ts |
PaymentFactory.ts |
StorageFactory.ts |
None |
| Factory style | Static class method | Static class method | Module function | N/A |
| Provider naming | XxxService |
StripeService |
XxxStorage |
NodeCacheService |
| Singleton | Yes | No | Yes | N/A |
Error Handling Philosophy¶
Inconsistent across all 4 subsystems: - NodeMailerService: Catches and rejects via Promise - ResendService: No try/catch — raw propagation - BrevoService: Catches and swallows (CRITICAL) - StripeService: Optional chaining returns undefined (CRITICAL) - StorageFactory: Throws on unknown provider - Cache: No error handling at all
Testing Strategy¶
ZERO test coverage across all 4 factory subsystems. No test files, no test dependencies, no test scripts.
Data Flow¶
Email Data Flow¶
Event Handler (register/invite)
→ EmailFactory.getEmailService() ← BROKEN (EMAIL_SERVICE missing)
→ NodeMailerService.sendEmail() → SMTP server
→ ResendService.sendEmail() → Resend HTTP API
→ BrevoService.sendEmail() → Brevo API (errors swallowed)
Legacy path (helper/email.ts) ← WORKING
→ Event Handler (forgot-pass/login-otp/invite-only)
→ sendEmail() → nodemailer transporter → SMTP server
Payment Data Flow¶
payment.controller.ts
→ paymentService.createPaymentForSubscription(paymentGateway, options)
→ PaymentFactory.create("stripe") ← paymentGateway IGNORED
→ StripeService.createCheckout()
→ stripe.checkout.sessions.create() → Stripe API
→ redirect to success_url / cancel_url (cancel_url BROKEN)
Webhook path:
→ webhook.controller.ts
→ stripe.webhooks.constructEvent() (signature verification)
→ stripe.service.ts handlers (business operations)
→ getStripe() → AppConfig DB (encrypted) or env var
Storage Data Flow (Production — Helper Layer)¶
Controller (Blog/User/Setting/GenericPage/Product)
→ handleFileUpload(file, storageType)
→ if "cloudinary": uploadImage() → uploadToCloudinary() → Cloudinary API
→ if "s3": cloud/s3.ts → AWS S3 API
→ if "local": returns URL only (DOES NOT write file)
→ Controller must call saveBufferToFile() manually
Cache Data Flow¶
Role service → shared/cache.ts (set/get/del with TTL)
MenuItem service → shared/cache.ts (set/get/del with TTL)
contextMiddleware → shared/permissionCache.ts (AsyncLocalStorage, per-request, no TTL)
Integration Points¶
External APIs Consumed¶
| API | Provider | Used By | Auth Method |
|---|---|---|---|
| SMTP (port 587/465) | Gmail/custom | NodeMailerService, helper/email.ts | MAIL_USER + MAIL_PASS |
| Resend API | Resend | ResendService | RESEND_API_KEY (via MAIL_PASS) |
| Brevo API | Brevo | BrevoService | BREVO_API_KEY |
| Stripe API | Stripe | StripeService, stripe.service.ts | STRIPE_SECRET or AppConfig DB |
| LemonSqueezy API | LemonSqueezy | getLemonsqueezy.ts | LEMONSQUEEZY_API_KEY |
| AWS S3 | Amazon | AwsStorage (dead), helper/cloud/s3.ts | AWS_ACCESS_KEY_ID + SECRET |
| Cloudinary API | Cloudinary | CloudinaryStorage (dead), helper/uploadToCloudinary | CLOUDINARY_* keys |
Env Var Map (Complete)¶
| Env Var | Used By | Present in .env | Present in .env.example | Notes |
|---|---|---|---|---|
| EMAIL_SERVICE | EmailFactory | NO | NO | MISSING — breaks factory |
| MAIL_TYPE | helper/email.ts | NO | YES | Legacy email system selector |
| MAIL_HOST | NodeMailerService, transporterMail | YES | YES | SMTP host |
| MAIL_PORT | NodeMailerService, transporterMail | YES | YES | SMTP port |
| MAIL_USER | NodeMailerService, ResendService, BrevoService | YES | YES | SMTP user / API from |
| MAIL_PASS | NodeMailerService, ResendService | YES | YES | SMTP pass / API key |
| MAIL_FROM | NOBODY | YES | NO | Configured but unused |
| RESEND_API_KEY | helper/email/resend.ts | NO | YES | Not used by factory |
| BREVO_API_KEY | BrevoService | NO | NO | Missing everywhere |
| STRIPE_SECRET | config/stripe.ts | YES | YES | Primary Stripe key |
| STRIPE_PUBLISHED_KEY | Frontend | YES | YES | Client-side key |
| STRIPE_LOCAL_WEBHOOK_SECRET | webhook.controller.ts | YES | NO | Dev webhook verify |
| STRIPE_PROD_WEBHOOK_SECRET | webhook.controller.ts | YES | NO | Prod webhook verify |
| LEMONSQUEEZY_API_KEY | getLemonsqueezy.ts | YES | YES | LS API key |
| LEMONSQUEEZY_STORE_ID | getLemonsqueezy.ts | YES | YES | Checked but unused |
| LEMONSQUEEZY_WEBHOOK_SECRET | REPLACED | N/A | N/A | Hardcoded in source |
| IMAGE_STORAGE | StorageFactory (dead), handleFileUpload | YES | YES | Default: "local" |
| AWS_ACCESS_KEY_ID | AwsStorage (dead), cloud/s3.ts | YES | YES | S3 auth |
| AWS_SECRET_ACCESS_KEY | AwsStorage (dead), cloud/s3.ts | YES | YES | S3 auth |
| AWS_REGION | AwsStorage (dead), cloud/s3.ts | YES | YES | S3 region |
| AWS_BUCKET_NAME | AwsStorage (dead), cloud/s3.ts | YES | YES | S3 bucket |
| CLOUDINARY_CLOUD_NAME | CloudinaryStorage (dead), config/cloudinary.ts | YES | YES | Cloudinary auth |
| CLOUDINARY_API_KEY | CloudinaryStorage (dead), config/cloudinary.ts | YES | YES | Cloudinary auth |
| CLOUDINARY_API_SECRET | CloudinaryStorage (dead), config/cloudinary.ts | YES | YES | Cloudinary auth |
| MASTER_KEY | config/getStripe.ts | YES | YES | AES-256-GCM encryption key |
| CLIENT_URL | StripeService, getLemonsqueezy | YES | YES | Redirect URLs |
AppConfig Relationship¶
Only Stripe has DB-stored encrypted secret support:
- config/getStripe.ts → AppConfigService.getConfigByKey("stripe_secret_key") → AES-256-GCM decrypt with MASTER_KEY
- All other secrets (SMTP password, Resend key, Brevo key, Cloudinary secret, AWS secret, LemonSqueezy key) are env-var-only, plaintext
Dependency Graph¶
Entry Points (Not Imported by Others in Scope)¶
EmailFactory.ts(called by event handlers)paymentService.ts(called by payment controller)StorageFactory.ts(DEAD — no callers)Node-cache.ts(DEAD — no callers)
Leaf Nodes (Don't Import Others in Scope)¶
IEmailService.tspayment.type.tsIStorageService.tscache.interface.tstemplate/signupEmailVerification.ts
Circular Dependencies¶
config/getStripe.ts↔modules/v1/stripe.service.ts— Fragile circular import at startup
Testing Analysis¶
Test Coverage Summary¶
- Statements: 0%
- Branches: 0%
- Functions: 0%
- Lines: 0%
Test Files¶
None exist for any factory subsystem.
Testing Gaps¶
- No unit tests for any email provider
- No unit tests for factory selection logic
- No integration tests for email delivery
- No unit tests for Stripe checkout/verify/portal flows
- No tests for null-Stripe error paths
- No tests for webhook signature verification
- No tests for storage upload/download/delete
- No tests for cache set/get/del/TTL behavior
- No tests for concurrent cache access
- No tests for cache key collision
Related Code & Reuse Opportunities¶
Duplicate Implementations¶
| Functionality | Factory Version | Legacy Version | Status |
|---|---|---|---|
| Send email | lib/email/EmailFactory |
helper/email.ts |
Both in production, different config keys |
| Stripe init | lib/payment/StripeService constructor |
config/stripe.ts, config/getStripe.ts |
3 separate init paths |
| S3 upload | lib/storage/AwsStorage |
helper/cloud/s3.ts |
Factory dead, helper in production |
| Cloudinary upload | lib/storage/CloudinaryStorage |
helper/uploadToCloudinary.ts |
Factory dead, helper in production |
| Local file write | lib/storage/LocalSotrage |
helper/saveBufferToFile.ts |
Factory dead (and broken), helper in production |
| Cache get/set | lib/cache/Node-cache.ts |
shared/cache.ts |
Factory dead, shared in production |
Consolidation Opportunities¶
- Email: Unify to single factory with
MAIL_TYPE/EMAIL_SERVICEmerger. Remove deademail.ts,transporterMail.ts,signupEmailVerification.ts. Fix BrevoService error handling. - Payment: Complete LemonSqueezy implementation or remove type. Fix
paymentGatewaypassthrough. Consolidate triple Stripe init to single path. - Storage: Either migrate helpers to factory pattern or delete entire
lib/storage/. FixhandleFileUploadlocal mode. - Cache: Either migrate
shared/cache.tstolib/cache/pattern or deletelib/cache/. Fix TTL mismatches.
Dead Code Summary¶
| File | LOC | Subsystem | Reason |
|---|---|---|---|
lib/email/email.ts |
~33 | Zero importers | |
lib/email/transporterMail.ts |
~18 | Only imported by dead email.ts | |
lib/email/template/signupEmailVerification.ts |
~13 | Zero importers | |
lib/storage/StorageFactory.ts |
~27 | Storage | Zero consumers |
lib/storage/IStorageService.ts |
~10 | Storage | Only used by dead factory |
lib/storage/AwsStorage.ts |
~89 | Storage | Only used by dead factory |
lib/storage/CloudinaryStorage.ts |
~74 | Storage | Only used by dead factory |
lib/storage/LocalSotrage.ts |
~50 | Storage | Never imported (typo) |
lib/cache/cache.interface.ts |
~5 | Cache | Zero consumers |
lib/cache/Node-cache.ts |
~32 | Cache | Zero consumers |
createStripePaymentIntent |
~33 | Payment | Never called (references "paypal") |
| Various dead imports | ~50+ | Payment | Trigger unnecessary SDK evaluation |
| TOTAL | ~434+ |
Known Issues — Complete Catalog¶
Critical (28)¶
| # | ID | Subsystem | Title |
|---|---|---|---|
| 1 | E-F1 | EMAIL_SERVICE missing from .env — factory always throws |
|
| 2 | E-B1 | BrevoService silently swallows errors | |
| 3 | E-ST1 | signupEmailVerification.ts zero importers (dead code) | |
| 4 | E-D1 | Split-brain email: factory (EMAIL_SERVICE) vs helper (MAIL_TYPE) | |
| 5 | E-D2 | Registration/invite emails broken by default | |
| 6 | E-D3 | Real SMTP credentials committed to .env | |
| 7 | E-D4 | MAIL_FROM configured but unused by all providers | |
| 8 | P-6 | Payment | "lemonsqueezy" accepted by TypeScript but throws at runtime |
| 9 | P-9 | Payment | cancelUrl uses {CHECKOUT_SESSION_ID} — Stripe doesn't replace in cancel_url |
| 10 | P-10 | Payment | Optional chaining returns undefined URL instead of throwing |
| 11 | P-21 | Payment | Hardcoded localhost:5500 in LemonSqueezy redirect |
| 12 | P-22 | Payment | LemonSqueezy user_id set to email instead of userId |
| 13 | P-26 | Payment | stripeProductId column doesn't exist on Package entity |
| 14 | P-32 | Payment | paymentGateway parameter completely ignored (hardcoded "stripe") |
| 15 | P-39 | Payment | Hardcoded LemonSqueezy webhook secret in source code |
| 16 | S-4 | Storage | Factory crashes on default config "local" — no case in switch |
| 17 | S-5 | Storage | Entire lib/storage/ is dead code (zero consumers) |
| 18 | S-18 | Storage | CloudinaryStorage missing implements IStorageService |
| 19 | S-25 | Storage | Filename typo "LocalSotrage.ts" |
| 20 | S-26 | Storage | LocalStorage.upload() writes base64 text not binary — files corrupted |
| 21 | S-27 | Storage | Path traversal vulnerability in LocalStorage |
| 22 | S-29 | Storage | saveAfterUpload() swaps arguments (folderName↔fileName) |
| 23 | S-35 | Storage | Two independent upload systems share NO code |
| 24 | CA-C1 | Cache | lib/cache/ entirely dead code (zero consumers) |
| 25 | CA-C2 | Cache | Comment says "5 minutes" but code is 3600s (1 hour) |
| 26 | CA-C3 | Cache | Two NodeCache instances with different useClones behavior |
| 27 | CA-C4 | Cache | TTL 60×24=1440s (24 min) but comments say "1 day" — off by 60x |
| 28 | CA-C5 | Cache | Dual cache infrastructure with no migration path |
High (33)¶
| # | ID | Subsystem | Title |
|---|---|---|---|
| 1 | E-N1 | TLS cert validation disabled (rejectUnauthorized: false) | |
| 2 | E-TM1 | TLS cert validation disabled (transporterMail) | |
| 3 | E-N2 | Explicit Promise(async) anti-pattern | |
| 4 | E-E1 | 'use server' directive in Express backend | |
| 5 | E-E2 | Dev mode silently swallows email failures | |
| 6 | E-R1 | Unsafe as string cast in ResendService |
|
| 7 | E-B2 | Dead test email object in BrevoService | |
| 8 | E-B5 | Unsafe as string cast in BrevoService |
|
| 9 | P-7 | Payment | No singleton/caching in PaymentFactory |
| 10 | P-11 | Payment | Methods leak outside IPaymentService interface |
| 11 | P-15 | Payment | Dual Stripe initialization paths |
| 12 | P-17 | Payment | Circular dependency getStripe ↔ stripe.service |
| 13 | P-18 | Payment | No cache invalidation for Stripe key rotation |
| 14 | P-23 | Payment | LemonSqueezy not in PaymentFactory |
| 15 | P-27 | Payment | isDeleted filter on @DeleteDateColumn entity |
| 16 | P-28 | Payment | Billing portal config created every call |
| 17 | P-33 | Payment | Dead createStripePaymentIntent (33 LOC, "paypal" reference) |
| 18 | P-34/35 | Payment | Dead imports trigger SDK evaluation |
| 19 | P-40 | Payment | Stripe error response leaks error object |
| 20 | S-6 | Storage | No LocalSotrage import in factory |
| 21 | S-9 | Storage | Pointless base64 round-trip in AwsStorage |
| 22 | S-10 | Storage | Mixed config sources in AwsStorage |
| 23 | S-11 | Storage | No filename sanitization in AwsStorage |
| 24 | S-19 | Storage | Base64 string to upload_stream in Cloudinary |
| 25 | S-28 | Storage | saveAfterUploads() ignores fileName param |
| 26 | S-36 | Storage | handleFileUpload "local" doesn't write file |
| 27 | S-37 | Storage | Three different S3 implementations |
| 28 | S-38 | Storage | Inconsistent storage type names ("s3" vs "aws") |
| 29 | S-40 | Storage | Hardcoded image/jpeg MIME type |
| 30 | CA-H1 | Cache | ICache missing delByPrefix() (most-used method) |
| 31 | CA-H2 | Cache | No maxKeys limit — unbounded memory growth |
| 32 | CA-H3 | Cache | JSON double-serialization (stringify + useClones deep-clone) |
| 33 | CA-H4 | Cache | No cross-request permission caching — every auth hits DB |
Medium (46)¶
| # | ID | Subsystem | Title |
|---|---|---|---|
| 1 | E-N3 | Hardcoded from address ("Sass" typo, example.com) | |
| 2 | E-E3 | Duplicate IEmailSendPayload definition | |
| 3 | E-E4 | Hardcoded "Sass" from in email.ts | |
| 4 | E-R2 | Zero error handling in ResendService | |
| 5 | E-R3 | Wrong from address source (SMTP user for Resend) | |
| 6 | E-B3 | Unused imports in BrevoService | |
| 7 | E-B4 | setApiKey on every send | |
| 8 | E-I1 | No error contract on IEmailService | |
| 9 | E-ST2 | XSS risk in template (verifyUrl not escaped) | |
| 10 | E-F2 | console.log on every factory call | |
| 11 | P-1 | Payment | amount field never consumed |
| 12 | P-4 | Payment | Interface too thin (no refund/cancel/customer) |
| 13 | P-8 | Payment | Factory doesn't read from config |
| 14 | P-12 | Payment | verify() silently returns false when null |
| 15 | P-13 | Payment | No idempotency keys |
| 16 | P-16 | Payment | config/stripe.ts still imported by 5 files |
| 17 | P-19 | Payment | Decryption crash not caught |
| 18 | P-24 | Payment | storeId/webhookSecret validated but unused |
| 19 | P-25 | Payment | LemonSqueezy SDK re-initialized every call |
| 20 | P-29 | Payment | validPriceId dead code |
| 21 | P-30 | Payment | Dead stripe import in stripe.service.ts |
| 22 | P-36 | Payment | createPayament typo |
| 23 | P-37 | Payment | Metadata string-to-number inconsistent defaults |
| 24 | P-41 | Payment | Duplicate timingSafeEqual check |
| 25 | S-1 | Storage | key optional in return, required for delete() |
| 26 | S-7 | Storage | Singleton not resettable for tests |
| 27 | S-12 | Storage | No file size/type validation (AwsStorage) |
| 28 | S-13 | Storage | URL assumes public S3 bucket |
| 29 | S-14 | Storage | No error handling/retry for S3 |
| 30 | S-15 | Storage | No concurrency limit on batch (AWS) |
| 31 | S-20 | Storage | Hardcoded "uploads" folder (Cloudinary) |
| 32 | S-21 | Storage | delete() ignores result (Cloudinary) |
| 33 | S-22 | Storage | No file type validation (Cloudinary) |
| 34 | S-30 | Storage | No directory creation before write (Local) |
| 35 | S-31 | Storage | fs.unlinkSync blocks event loop |
| 36 | S-32 | Storage | No overwrite protection (Local) |
| 37 | S-39 | Storage | Inconsistent storage type names ("s3" vs "aws") |
| 38 | S-41 | Storage | Force-converts all uploads to WebP |
| 39 | S-42 | Storage | uploadImage only supports Cloudinary |
| 40 | CA-M2 | Cache | revalidateCache non-atomic (del+set miss window) |
| 41 | CA-M3 | Cache | addPermissionToCache allows runtime injection |
| 42 | CA-M4 | Cache | Analytic service dead cache import |
| 43 | CA-M5 | Cache | getRoleList type mismatch |
| 44 | CA-M6 | Cache | getMenuItemList type mismatch |
| 45 | CA-M7 | Cache | No cache warming strategy |
| 46 | E-TM2 | Duplicate transporterMail.ts creation |
Low (40)¶
| # | ID | Subsystem | Title |
|---|---|---|---|
| 1 | E-T1 | Single recipient only (no array/CC/BCC) | |
| 2 | E-T2 | No from field in payload type |
|
| 3 | E-N4 | transPorter casing inconsistency |
|
| 4 | E-N5 | verify() on every send (latency) | |
| 5 | E-R4 | Redundant html spread | |
| 6 | E-R5 | No attachment support (factory) | |
| 7 | E-ST3 | ${"there"} pointless template expression |
|
| 8 | E-ST4 | Only 1 template in folder | |
| 9 | E-TM3 | Dead CommonJS export comment | |
| 10 | E-TM4 | Transitively dead file | |
| 11 | E-I2 | No batch/attachment/CC/BCC in interface | |
| 12 | E-F3 | No runtime type narrowing in factory | |
| 13 | E-B6 | Hardcoded sender "Your App" in dead code | |
| 14 | E-B7 | BREVO_API_KEY absent from env files | |
| 15 | P-2 | Payment | planId number branch misleading for Stripe |
| 16 | P-3 | Payment | metadata typed as Record |
| 17 | P-5 | Payment | verify return uses any for data |
| 18 | P-14 | Payment | products: any[] untyped |
| 19 | P-20 | Payment | Console output uses emoji |
| 20 | P-31 | Payment | Mixed function declaration styles |
| 21 | P-38 | Payment | Unnecessary as number cast |
| 22 | P-42 | Payment | Dead import of sync stripe in webhook |
| 23 | S-2 | Storage | commit()/rollback() never implemented |
| 24 | S-3 | Storage | No download/exists methods |
| 25 | S-8 | Storage | Function vs file name mismatch (StorageFactory) |
| 26 | S-16 | Storage | Key collision risk (Date.now()) |
| 27 | S-17 | Storage | No content disposition on S3 uploads |
| 28 | S-23 | Storage | Unnecessary non-null assertions (Cloudinary) |
| 29 | S-24 | Storage | No concurrency limit on batch (Cloudinary) |
| 30 | S-33 | Storage | saveAfterUpload/saveAfterUploads not on interface |
| 31 | S-34 | Storage | Partial fs usage (Local) |
| 32 | S-dead1 | Storage | Dead imports in image.service.ts |
| 33 | S-dead2 | Storage | Triple Cloudinary configuration |
| 34 | CA-L1 | Cache | validateCacheOwnership returns true for "no cache" |
| 35 | CA-L2 | Cache | Triple userId validation |
| 36 | CA-L3 | Cache | getCacheStats double-fetches getPermissionCache |
| 37 | CA-L4 | Cache | set() uses if (ttlSeconds) — falsy for 0 |
| 38 | E-E5 | verify() on every send in email.ts | |
| 39 | CA-L10 | Cache | All ICache methods async for sync ops |
| 40 | E-TM-dup | Two transporterMail with different options |
Modification Guidance¶
To Add a New Email Provider¶
- Create
src/lib/email/NewProvider.tsimplementingIEmailService - Add
case "newprovider"toEmailFactory.getEmailService()switch - Add
EMAIL_SERVICE=newproviderto.envand.env.example - WARNING: Must also update
helper/email.tsif any consumers use the legacy path - Ensure error handling matches expected behavior (throw on failure, do NOT swallow)
To Add a New Payment Provider¶
- Create
src/lib/payment/NewPaymentService.tsimplementingIPaymentService - Add
case "newprovider"toPaymentFactory.create()switch - Fix
paymentService.tsto passpaymentGatewayto factory (currently hardcoded) - Update
PaymentProvidertype inpayment.type.ts
To Unify Storage Systems¶
- Option A (Migrate to factory): Wire
handleFileUploadconsumers to useStorageFactory→ fix factory bugs → add "local" case → fix LocalSotrage filename → fix base64 write - Option B (Delete factory): Remove entire
lib/storage/(250 LOC) → document helper layer as canonical → fix helper layer bugs
To Unify Cache Systems¶
- Option A (Migrate to lib/cache): Create CacheFactory → migrate
shared/cache.tsconsumers → fix TTL mismatches → add maxKeys → fix useClones - Option B (Delete lib/cache): Remove
lib/cache/(37 LOC) → fixshared/cache.tsTTL comments → add maxKeys limit
Testing Checklist for Changes¶
- Verify EMAIL_SERVICE env var is set before testing email factory
- Test each email provider with mock SMTP/API
- Verify Stripe checkout creates session with correct URLs
- Test cancel URL redirect (currently broken)
- Verify null-Stripe throws instead of returning undefined
- Test storage upload with each provider type
- Verify local storage writes binary not base64 text
- Test cache TTL matches intended values (not comments)
- Verify cache doesn't grow unbounded under load
- Test webhook signature verification for both Stripe and LemonSqueezy
- Verify BrevoService propagates errors to callers
Systemic Findings¶
-
Incomplete Migration: The
lib/factories appear to be a refactoring that was ~11% completed. Two subsystems (storage, cache) were never wired up. Email was partially wired (2 of 6 consumers). Payment was minimally wired (1 consumer that defeats the purpose by hardcoding). -
Zero Test Coverage: None of the 4 factory subsystems have any test files. This is the root cause of most bugs persisting (silent failures, wrong URLs, dead code, swapped arguments).
-
Secret Management Gap: Only Stripe supports encrypted DB-stored secrets (via AppConfig). All other API keys are plaintext env vars. Real SMTP credentials are committed to
.env. -
No Factory for Cache: Unlike email, payment, and storage, cache has no factory pattern at all — just a dead OOP wrapper and a production functional singleton.
-
Console.log in Production: Multiple factory methods log to stdout on every call (EmailFactory, NodeMailerService, BrevoService).
-
Architectural Drift: Each factory follows slightly different patterns (class vs function factory, singleton vs new-per-call, types in separate file vs bundled), making it hard to reason about the system holistically.
Subagent Reports¶
Detailed per-subsystem findings are available at:
- _bmad-output/extra-docs/email-factory-deep-dive/ — 39 issues (7C, 8H, 10M, 14L)
- _bmad-output/extra-docs/payment-factory-deep-dive/ — 42 issues (7C, 10H, 14M, 11L)
- _bmad-output/extra-docs/storage-factory-deep-dive/ — 42 issues (8C, 9H, 14M, 11L)
- _bmad-output/extra-docs/cache-deep-dive/ — 22 issues (5C, 6H, 7M, 4L)
- _bmad-output/extra-docs/factory-cross-cutting-analysis/ — Cross-factory consumer mapping, competing systems inventory, AppConfig analysis
Generated by document-project workflow (deep-dive mode) | Exhaustive scan with 5 parallel subagents
Base Documentation: docs/index.md
Scan Date: 2026-03-29
Analysis Mode: Exhaustive