mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 18:08:45 +01:00
adds a /pay/{token} redirect endpoint that generates a short URL
pointing to a stripe checkout sessions in setup mode. The TUI shows a QR
code and URL on the payment page so users can add a card via the browser
instead of entering raw card data over SSH.
the checkout.session.completed webhook saves the card to the database.
the TUI polls GET /api/v1/cards every second and auto-proceeds to
confirmation once the new card is detected.
also adds a dev.sh startup script that runs all four required processes.
(SSH server, API server, Stripe CLI listener, ngrok) in a single
terminal
125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
// Package audit writes structured JSON logs for financial events.
|
|
// Every card addition, order creation, payment, and refund is recorded here.
|
|
// Pipe this output to your log aggregator (Datadog, Loki, CloudWatch, etc.).
|
|
package audit
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
type eventType string
|
|
|
|
const (
|
|
eventCardAdded eventType = "card_added"
|
|
eventCardDeleted eventType = "card_deleted"
|
|
eventOrderCreated eventType = "order_created"
|
|
eventOrderPaid eventType = "order_paid"
|
|
eventOrderFailed eventType = "order_failed"
|
|
eventOrderRefunded eventType = "order_refunded"
|
|
eventPaymentCritical eventType = "payment_critical"
|
|
)
|
|
|
|
type event struct {
|
|
Time time.Time `json:"time"`
|
|
Event eventType `json:"event"`
|
|
UserID uint `json:"user_id,omitempty"`
|
|
OrderID uint `json:"order_id,omitempty"`
|
|
CardID uint `json:"card_id,omitempty"`
|
|
Amount int `json:"amount_cents,omitempty"`
|
|
StripeID string `json:"stripe_id,omitempty"`
|
|
CardLast4 string `json:"card_last4,omitempty"`
|
|
CardBrand string `json:"card_brand,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Extra map[string]string `json:"extra,omitempty"`
|
|
}
|
|
|
|
func emit(e event) {
|
|
e.Time = time.Now().UTC()
|
|
b, err := json.Marshal(e)
|
|
if err != nil {
|
|
log.Printf("[audit] failed to marshal event: %v", err)
|
|
return
|
|
}
|
|
log.Printf("[AUDIT] %s", b)
|
|
}
|
|
|
|
// CardAdded records that a user saved a new payment method.
|
|
func CardAdded(userID, cardID uint, brand, last4 string) {
|
|
emit(event{
|
|
Event: eventCardAdded,
|
|
UserID: userID,
|
|
CardID: cardID,
|
|
CardBrand: brand,
|
|
CardLast4: last4,
|
|
})
|
|
}
|
|
|
|
// CardDeleted records that a user removed a saved payment method.
|
|
func CardDeleted(userID, cardID uint) {
|
|
emit(event{
|
|
Event: eventCardDeleted,
|
|
UserID: userID,
|
|
CardID: cardID,
|
|
})
|
|
}
|
|
|
|
// OrderCreated records that an order record was created (before payment).
|
|
func OrderCreated(userID, orderID uint, amountCents int) {
|
|
emit(event{
|
|
Event: eventOrderCreated,
|
|
UserID: userID,
|
|
OrderID: orderID,
|
|
Amount: amountCents,
|
|
})
|
|
}
|
|
|
|
// OrderPaid records a successful charge.
|
|
func OrderPaid(userID, orderID uint, amountCents int, stripePaymentIntentID string) {
|
|
emit(event{
|
|
Event: eventOrderPaid,
|
|
UserID: userID,
|
|
OrderID: orderID,
|
|
Amount: amountCents,
|
|
StripeID: stripePaymentIntentID,
|
|
})
|
|
}
|
|
|
|
// OrderFailed records a failed charge attempt.
|
|
func OrderFailed(userID, orderID uint, amountCents int, reason string) {
|
|
emit(event{
|
|
Event: eventOrderFailed,
|
|
UserID: userID,
|
|
OrderID: orderID,
|
|
Amount: amountCents,
|
|
Error: reason,
|
|
})
|
|
}
|
|
|
|
// OrderRefunded records a refund.
|
|
func OrderRefunded(userID, orderID uint, amountCents int, stripeRefundID string) {
|
|
emit(event{
|
|
Event: eventOrderRefunded,
|
|
UserID: userID,
|
|
OrderID: orderID,
|
|
Amount: amountCents,
|
|
StripeID: stripeRefundID,
|
|
})
|
|
}
|
|
|
|
// PaymentCritical records the dangerous state where Stripe charged the card
|
|
// but the local database transaction failed. This MUST be investigated and
|
|
// manually reconciled. Emit a loud log line that monitoring can alert on.
|
|
func PaymentCritical(orderID uint, stripePaymentIntentID string, err error) {
|
|
emit(event{
|
|
Event: eventPaymentCritical,
|
|
OrderID: orderID,
|
|
StripeID: stripePaymentIntentID,
|
|
Error: err.Error(),
|
|
})
|
|
// Second line so it's impossible to miss in plain-text log tails.
|
|
log.Printf("[AUDIT][CRITICAL] order %d was charged (pi=%s) but DB commit failed: %v — REQUIRES MANUAL RECONCILIATION",
|
|
orderID, stripePaymentIntentID, err)
|
|
}
|