39 lines
1.8 KiB
Python
39 lines
1.8 KiB
Python
"""Mock TEST-MODE payment provider. Never contacts a real processor, never takes real money.
|
|
|
|
Only well-known test card numbers are accepted; any other number is rejected so a real
|
|
card can never be "charged" even by accident. Card numbers are not stored (last4 only).
|
|
"""
|
|
import hashlib, hmac, json, urllib.request
|
|
from .store import now, new_id
|
|
|
|
TEST_CARDS = {
|
|
"4242424242424242": ("succeeded", None),
|
|
"4000000000000002": ("failed", "card_declined"),
|
|
"4000000000009995": ("failed", "insufficient_funds"),
|
|
}
|
|
|
|
|
|
def sign(secret: str, body: bytes) -> str:
|
|
return hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
|
|
|
|
|
def charge(intent: dict, card_number: str, webhook_url: str, secret: str) -> dict:
|
|
"""Simulate the provider: decide outcome, then deliver a signed webhook to the platform."""
|
|
digits = "".join(ch for ch in str(card_number or "") if ch.isdigit())
|
|
if digits not in TEST_CARDS:
|
|
return {"status": "failed", "error": "not_a_test_card",
|
|
"message": "TEST MODE: only test cards are accepted (use 4242 4242 4242 4242)."}
|
|
status, err = TEST_CARDS[digits]
|
|
event = {
|
|
"id": new_id("evt"), "type": "payment_intent." + ("succeeded" if status == "succeeded" else "payment_failed"),
|
|
"created": now(), "livemode": False,
|
|
"data": {"intent_id": intent["id"], "amount_cents": intent["total_cents"], "fee_cents": intent["fee_cents"],
|
|
"card_last4": digits[-4:], "failure_code": err},
|
|
}
|
|
body = json.dumps(event).encode()
|
|
req = urllib.request.Request(webhook_url, data=body, method="POST", headers={
|
|
"Content-Type": "application/json", "X-Mock-Signature": sign(secret, body)})
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
ack = json.loads(r.read().decode())
|
|
return {"status": status, "error": err, "event_id": event["id"], "webhook_ack": ack}
|