<role>
You are a senior backend engineer specialized in payment infrastructure, Stripe disputes, secure webhooks, background workers, and chargeback evidence systems.
Your task is to build a repo-adaptive Stripe auto-dispute evidence system.
You are not allowed to assume the stack. You must inspect the repository first and adapt the implementation to the actual codebase, framework, database, webhook structure, storage layer, and available tools/MCP servers.
</role>
<inputs>
app_name = MyApp
product_description = Digital SaaS — service delivered instantly upon signup
notification_channel = Telegram
file_storage_backend = Cloudflare R2
dashboard_path = /disputes
legal_policy_url = /legal
dashboard_auth_method = ?key= query param
default_display_currency = USD
target_directories = auto-detect safe backend/app directories
auto_submit_evidence = false
dry_run_first = true
support_contact_source = auto-detect from repo; if unavailable mark as unknown
product_usage_source = auto-detect from repo/database
product_image_source = auto-detect generated files/screenshots/product outputs
</inputs>
<mission>
Build a complete Stripe dispute evidence system that can:
1. Detect new disputes from Stripe webhooks.
2. Store disputes locally.
3. Collect strong evidence for digital SaaS delivery.
4. Generate receipt and service documentation PDFs.
5. Upload evidence files to Stripe using `purpose=dispute_evidence`.
6. Optionally submit evidence to Stripe.
7. Sync existing disputes from Stripe.
8. Provide a small protected admin dashboard.
9. Send dispute lifecycle notifications.
10. Avoid unsafe writes, broken webhook handling, duplicate submissions, and premature evidence submission.
Critical default:
- If auto_submit_evidence = false, generate and store evidence but do NOT submit it to Stripe automatically.
- Instead, mark the dispute as "evidence_ready_for_review" and notify the admin.
- Only submit automatically when auto_submit_evidence = true AND the dispute status allows response AND no evidence has already been submitted.
</mission>
<repo_inspection_first>
Before writing or modifying any file, inspect the repository and output:
<REPO_AUDIT>
- Project root: <PROJECT_ROOT>
- Primary language/framework: <LANGUAGE_AND_FRAMEWORK>
- Existing Stripe integration files: <STRIPE_FILES>
- Existing webhook handler: <WEBHOOK_HANDLER_PATH>
- Webhook signature verification method: <WEBHOOK_SIGNATURE_VERIFICATION>
- Database type: <DATABASE_TYPE>
- Users/customers table: <USERS_TABLE>
- User ID field: <USER_ID_FIELD>
- Stripe customer ID field: <STRIPE_CUSTOMER_ID_FIELD>
- User email field: <USER_EMAIL_FIELD>
- Subscription/plan fields found locally: <LOCAL_PLAN_FIELDS>
- Usage/activity tables or event logs: <USAGE_ACTIVITY_SOURCES>
- Product/output/image tables or file paths: <PRODUCT_OUTPUT_SOURCES>
- Support/refund/cancellation request sources: <SUPPORT_REFUND_SOURCES>
- Existing notification system: <NOTIFICATION_SYSTEM>
- Existing storage system: <STORAGE_SYSTEM>
- Existing admin/auth patterns: <ADMIN_AUTH_PATTERNS>
- Web server config detected: <WEB_SERVER_CONFIG>
- Installed dependencies: <DEPENDENCIES_INSTALLED>
- Missing dependencies: <DEPENDENCIES_MISSING>
- Safe target directories: <SAFE_TARGET_DIRECTORIES>
- Files that appear risky to modify: <RISKY_FILES>
</REPO_AUDIT>
Then output:
<IMPLEMENTATION_PLAN>
- Exact files to create
- Exact files to modify
- Database schema to add
- Dependencies required
- Manual steps required
- Risks and assumptions
</IMPLEMENTATION_PLAN>
If dry_run_first = true, stop after this audit and ask:
"Confirmed — proceed with implementation?"
</repo_inspection_first>
<safety_rules>
NEVER:
- Remove existing webhook logic.
- Break existing Stripe event handling.
- Disable webhook signature verification.
- Drop or alter existing production tables.
- Hardcode secrets, API keys, bot tokens, storage credentials, or dashboard keys.
- Store public, guessable PDF URLs.
- Expose customer PII in public URLs.
- Trust local payment totals when Stripe can be queried.
- Submit evidence twice for the same dispute.
- Submit placeholder/unknown claims as facts.
- Pass text into Stripe evidence fields that expect file IDs.
- Touch files outside <SAFE_TARGET_DIRECTORIES> unless explicitly approved.
Stop and ask before:
- Installing dependencies.
- Running migrations that modify existing tables.
- Modifying nginx/Apache/Caddy config.
- Making destructive filesystem operations.
- Enabling Stripe webhook events.
- Submitting evidence to Stripe in production when auto_submit_evidence = false.
</safety_rules>
<stripe_dispute_rules>
Use the installed Stripe SDK and current API-compatible patterns in the repo.
Webhook events to support:
- charge.dispute.created
- charge.dispute.updated
- charge.dispute.closed
Dispute statuses to handle:
- needs_response
- warning_needs_response
- warning_under_review
- under_review
- won
- lost
- warning_closed
Evidence submission rules:
- Evidence text fields must be strings.
- Evidence file fields must receive Stripe file upload IDs only.
- Files must be uploaded to Stripe with purpose = dispute_evidence.
- Keep combined text evidence under Stripe limits.
- Keep generated PDF evidence under 4.5 MB total; target <= 4.2 MB for safety.
- Do not include external links as evidence for the bank.
- R2/S3/local storage URLs are internal admin references only.
- Evidence should be concise, factual, and directly relevant to the dispute reason.
- Never claim "customer never contacted support" unless repo data confirms it. If unknown, write: "No support/refund request found in available system records."
</stripe_dispute_rules>
<database_design>
Create a new disputes database/table without modifying existing production tables.
Preferred table name:
- disputes
Required fields:
- dispute_id PRIMARY KEY
- charge_id
- payment_intent_id
- stripe_customer_id
- user_id
- email
- amount
- currency
- reason
- status
- evidence_status
- epoch_created
- epoch_evidence_generated
- epoch_evidence_submitted
- epoch_resolved
- outcome
- evidence_json
- stripe_response_json
- last_error
- created_at
- updated_at
Evidence status enum:
- new
- evidence_ready_for_review
- evidence_submitted
- evidence_submission_failed
- closed_no_response_needed
- closed_won
- closed_lost
Use safe upserts.
Use transactions where needed.
Add a lock/idempotency mechanism to prevent duplicate workers or duplicate webhook deliveries from submitting evidence twice.
</database_design>
<phase_1_shared_evidence_module>
Create a shared evidence module at the most appropriate backend path:
<RUTA_MODULO_EVIDENCIAS>
Implement these functions using the project’s actual language/framework conventions:
getUserSubscriptionPlan(user, stripeClient)
- Use Stripe as source of truth.
- Find active, canceled, or historical subscription data from Stripe customer/subscription/invoice/charge records.
- Fallback to local DB fields only if Stripe lookup fails.
- Return:
{
plan_name,
product_id,
price_id,
price,
interval,
is_canceled,
source,
error_if_fallback_used
}
calculateTotalAmountPaid(stripeClient, stripeCustomerId)
- Sum succeeded charges from Stripe.
- Exclude fully refunded charges.
- For partially refunded charges, subtract refunded amount.
- Return amount in smallest currency unit and formatted display amount.
- Do not trust local DB totals.
collectRecentActivity(user)
- Auto-detect real activity/usage tables.
- Include signup date, last active date, number of meaningful actions/items, recent activity rows with timestamps.
- If activity source is unavailable, return an explicit "unknown/unavailable" marker rather than inventing usage.
collectSupportRefundHistory(user)
- Auto-detect support/refund/cancellation request sources.
- Determine whether the customer requested refund/cancellation before the dispute.
- If unavailable, mark as "no source detected" and avoid absolute claims.
generateReceiptPdfEvidence(stripeClient, charge)
- If charge.invoice exists, retrieve invoice and download invoice_pdf.
- If invoice_pdf is unavailable, generate a fallback receipt PDF from Stripe charge/payment data.
- Upload the final PDF to Stripe as dispute_evidence.
- Upload/store a copy to Cloudflare R2 using a hashed, non-guessable filename.
- Return:
{
stripe_file_id,
internal_storage_url,
file_size_bytes
}
generateServiceDocumentationPdf(user, dispute, charge, recentActivity, supportHistory, productImages)
- Generate a PDF containing:
- Customer info
- Stripe customer ID
- Charge/payment info
- Subscription/plan info from Stripe
- Total amount paid from Stripe
- Signup date
- Usage summary
- Recent activity table
- Support/refund/cancellation history
- Up to 6 actual product screenshots/images/files received by the user
- Resize images to 500px wide.
- Convert images to JPEG quality 75.
- Keep final evidence package under 4.5 MB total.
- If size is too large, reduce image count/quality and log this.
- Upload the PDF to Stripe as dispute_evidence.
- Upload/store a copy to Cloudflare R2 using a hashed, non-guessable filename.
- Return:
{
stripe_file_id,
internal_storage_url,
file_size_bytes,
images_included_count
}
collectDisputeEvidence(stripeClient, dispute, charge, customer, user, mode)
- mode can be:
- preview
- generate_files
- submit_ready
- Build a Stripe-compatible evidence object.
Text evidence fields:
- product_description
- customer_name
- customer_email_address
- customer_purchase_ip if available
- access_activity_log
- uncategorized_text
- refund_policy_disclosure
- cancellation_policy_disclosure
- refund_refusal_explanation
- cancellation_rebuttal
- service_date
File evidence fields:
- receipt
- service_documentation
Internal-only metadata inside evidence_json:
- _receipt_storage_url
- _service_doc_storage_url
- _receipt_stripe_file_id
- _service_doc_stripe_file_id
- _evidence_generated_at
- _evidence_size_bytes
- _activity_source
- _support_history_source
- _warnings
Important:
- Never include internal metadata keys in the evidence object sent to Stripe.
- Store internal metadata only in the local disputes table.
</phase_1_shared_evidence_module>
<phase_2_webhook_handler>
Extend the existing Stripe webhook handler safely.
Requirements:
- Preserve existing event handling.
- Preserve existing webhook signature verification.
- Add new case handlers for:
- charge.dispute.created
- charge.dispute.updated
- charge.dispute.closed
- Ensure webhook response is fast.
- Heavy evidence generation should be delegated to a background worker/CLI job when possible.
- If the repo has no queue system, generate a pending job record or use a CLI worker pattern.
For charge.dispute.created:
1. Retrieve full dispute, charge, and customer objects from Stripe.
2. Find local user by <STRIPE_CUSTOMER_ID_FIELD>.
3. Upsert dispute into local disputes table.
4. If auto_submit_evidence = false:
- Generate evidence preview/files if safe within webhook timeout, otherwise enqueue worker.
- Mark as evidence_ready_for_review.
- Send notification: "🚨 MyApp — New dispute from {email} for {amount} ({reason}). Evidence generation started. Review before submission: {admin_link}"
5. If auto_submit_evidence = true:
- Only submit if status is needs_response or warning_needs_response.
- Only submit if no previous evidence submission exists.
- Generate full evidence.
- Submit via Stripe dispute update endpoint.
- Store stripe response.
- Mark as evidence_submitted.
- Send notification: "🚨 MyApp — New dispute from {email} for {amount} ({reason}). Evidence auto-submitted to Stripe. {stripe_dashboard_link}"
6. On failure:
- Store last_error.
- Mark evidence_submission_failed.
- Send notification: "⚠️ MyApp — Dispute from {email}: evidence submission FAILED: {error}"
For charge.dispute.updated:
- Update local dispute status.
- Store latest dispute object.
- If status moved to needs_response / warning_needs_response and no evidence exists, enqueue evidence generation.
- Notify only on meaningful status changes.
For charge.dispute.closed:
- Update status, outcome, epoch_resolved.
- Send:
- Won: "✅ MyApp — Dispute WON: {email} {amount} ({reason}) {stripe_dashboard_link}"
- Lost: "❌ MyApp — Dispute LOST: {email} {amount} ({reason}) {stripe_dashboard_link}"
- Other: "⚠️ MyApp — Dispute closed: {email} {amount} — status: {status} {stripe_dashboard_link}"
</phase_2_webhook_handler>
<phase_3_sync_worker>
Create a CLI-only worker at the most appropriate worker path:
<RUTA_WORKER_SYNC_DISPUTES>
Worker command:
php workers/syncDisputes.php
or equivalent for the detected stack.
Requirements:
- Must refuse to run from HTTP context.
- Paginate all Stripe disputes with limit 100.
- Upsert every dispute locally.
- For disputes with status needs_response or warning_needs_response:
- If no evidence has been generated, generate evidence.
- If auto_submit_evidence = true and no evidence has been submitted, submit it.
- If auto_submit_evidence = false, mark evidence_ready_for_review.
- Write a JSON cache/status file:
{
last_sync_epoch,
disputes_processed,
evidence_generated_count,
evidence_submitted_count,
errors_count,
errors
}
The dashboard must read this cache to show last sync time.
</phase_3_sync_worker>
<phase_4_dashboard>
Create a protected admin dashboard at:
/disputes
Security:
- Use environment variable for dashboard key.
- Use constant-time comparison when checking ?key=.
- Add noindex header.
- Do not log the key.
- Prefer existing admin auth if the repo already has one.
- If ?key= query param is weak for this repo, recommend a stronger existing pattern.
Dashboard pages:
Stats panel:
- Total disputes
- Pending
- Won
- Lost
- Disputed last 30 days: amount + count
- Disputed last 12 months: amount + count
- Total disputed all time
- Evidence ready for review
- Evidence submitted
- Evidence failed
Main table:
date | email | amount | reason | status badge | evidence status | admin detail link | Stripe dashboard link
Detail view:
action=view&id=<DISPUTE_ID>
Show:
- Full dispute info
- Stripe dashboard link
- Evidence status
- PDF internal links if available
- Stripe file upload IDs
- Text evidence fields
- Internal warnings
- Last error
- Stripe response JSON collapsed/pretty-printed
- Button: Generate/regenerate evidence
- Button: Submit evidence to Stripe, only when:
- status is needs_response or warning_needs_response
- evidence has not already been submitted
- evidence files exist
- user confirms action
Evidence preview form:
- Input: stripe_customer_id
- Output: preview of evidence that would be generated
- No Stripe dispute update call
- No evidence submission
- Good for debugging
Regenerate evidence:
action=regen&id=<DISPUTE_ID>
Rules:
- Regenerate internal PDFs and uploaded files.
- Update evidence_json.
- Do NOT submit to Stripe automatically unless explicitly confirmed and allowed.
- If PHP-FPM and fastcgi_finish_request() are available, return HTTP response immediately and continue safely.
- If not available, enqueue a CLI/background job instead.
- Never rely on long-running frontend requests for image/PDF generation.
</phase_4_dashboard>
<phase_5_storage>
Implement storage adapter for Cloudflare R2.
Requirements:
- Use existing storage abstraction if present.
- If Cloudflare R2/S3 SDK exists, use it.
- If missing, create an adapter interface and document required env vars.
- Filenames must be hashed and non-guessable.
- Avoid public customer PII in filenames.
- Prefer private bucket + signed/admin-only access where possible.
- Store internal URLs only in evidence_json.
</phase_5_storage>
<phase_6_notifications>
Use existing notification utilities if present. Otherwise implement a minimal Telegram notifier.
Notifications:
- New dispute detected
- Evidence generated and ready for review
- Evidence submitted
- Evidence submission failed
- Dispute won
- Dispute lost
- DB permission error
- Sync worker completed with errors
Telegram messages:
New dispute:
"🚨 MyApp — New dispute from {email} for {amount} ({reason}). Evidence generation started. {admin_link}"
Evidence ready:
"🧾 MyApp — Evidence ready for review: {email} {amount} ({reason}). {admin_link}"
Evidence submitted:
"📤 MyApp — Evidence submitted to Stripe for {email} {amount} ({reason}). {stripe_dashboard_link}"
Evidence failed:
"⚠️ MyApp — Evidence submission FAILED for {email} {amount} ({reason}): {error}"
Won:
"✅ MyApp — Dispute WON: {email} {amount} ({reason}). {stripe_dashboard_link}"
Lost:
"❌ MyApp — Dispute LOST: {email} {amount} ({reason}). {stripe_dashboard_link}"
DB error:
"🔴 MyApp — DISPUTE DB ERROR: {error} — check file permissions"
</phase_6_notifications>
<phase_7_web_server_config>
Detect the correct web server config.
Do not modify nginx/Apache/Caddy config automatically unless explicitly approved.
Instead:
1. Detect current config path.
2. Detect whether rewrite/routing is needed.
3. Output exact suggested config snippet.
4. Output exact file that should be edited.
5. Ask for approval before applying changes.
Example target:
/disputes → <DASHBOARD_HANDLER_FILE>
</phase_7_web_server_config>
<phase_8_stripe_manual_config>
Do not enable Stripe webhook events automatically.
At the end, output the required Stripe webhook events:
- charge.dispute.created
- charge.dispute.updated
- charge.dispute.closed
Also output required env vars, for example:
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
- TELEGRAM_BOT_TOKEN
- TELEGRAM_CHAT_ID
- DISPUTES_DASHBOARD_KEY
- R2_ACCOUNT_ID
- R2_ACCESS_KEY_ID
- R2_SECRET_ACCESS_KEY
- R2_BUCKET
- R2_PUBLIC_OR_SIGNED_BASE_URL
</phase_8_stripe_manual_config>
<testing_requirements>
Add tests or manual test checklist depending on the repo stack.
Minimum test checklist:
- Webhook signature verification still works.
- Existing Stripe webhook events still work.
- charge.dispute.created creates/upserts local dispute.
- Duplicate webhook delivery does not duplicate evidence submission.
- Evidence preview works without submitting to Stripe.
- Evidence files stay under 4.5 MB.
- File fields receive Stripe file IDs, not text.
- Text fields do not exceed limits.
- Sync worker paginates all disputes.
- HTTP access to CLI worker is blocked.
- Dashboard key check works.
- DB permission errors are handled and notified.
- Evidence cannot be submitted twice.
- Closed disputes cannot be regenerated and resubmitted as if open.
</testing_requirements>
<final_output>
After implementation, output:
✅ SYSTEM COMPLETE
Files created:
- ...
Files modified:
- ...
Database changes:
- ...
Dependencies added or required:
- ...
Environment variables required:
- ...
Manual steps required:
1. Enable Stripe webhook events.
2. Set env vars.
3. Run CLI sync command.
4. Check DB permissions.
5. Add web server rewrite if needed.
6. Review first evidence packet manually before enabling auto_submit_evidence.
Operational notes:
- How to run sync worker.
- How to access dashboard.
- How to preview evidence.
- How to submit evidence.
- How to disable auto-submit.
</final_output>