67a69ab073
- Go backend API with full CRUD for all modules - Rust analytics service with parallel processing - C runtime with POSIX shared memory IPC - PostgreSQL schema with 30+ tables - Redis cache, Kafka event streaming - WebSocket hub, automation engine - PDF generation, Resend email integration - JWT auth, multi-tenant - Docker Compose deployment - Nginx reverse proxy Refs: BOC-001
276 lines
8.7 KiB
Go
276 lines
8.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type SubscriptionHandler struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func NewSubscriptionHandler(db *sql.DB) *SubscriptionHandler {
|
|
return &SubscriptionHandler{DB: db}
|
|
}
|
|
|
|
type SubscriptionPlan struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
ProductID *string `json:"product_id"`
|
|
Interval string `json:"interval"`
|
|
IntervalCount int `json:"interval_count"`
|
|
Price float64 `json:"price"`
|
|
Currency string `json:"currency"`
|
|
TrialDays int `json:"trial_days"`
|
|
SetupFee float64 `json:"setup_fee"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type Subscription struct {
|
|
ID string `json:"id"`
|
|
CustomerID string `json:"customer_id"`
|
|
PlanID string `json:"plan_id"`
|
|
Status string `json:"status"`
|
|
StartDate time.Time `json:"start_date"`
|
|
EndDate *time.Time `json:"end_date"`
|
|
TrialEnd *time.Time `json:"trial_end"`
|
|
CurrentPeriodStart *time.Time `json:"current_period_start"`
|
|
CurrentPeriodEnd *time.Time `json:"current_period_end"`
|
|
Price float64 `json:"price"`
|
|
Currency string `json:"currency"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
func (h *SubscriptionHandler) ListPlans(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := h.DB.Query(`
|
|
SELECT id, name, description, product_id, interval, interval_count, price, currency, trial_days, setup_fee, status, created_at
|
|
FROM boc_subscription_plans WHERE status = 'active' ORDER BY name
|
|
`)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
plans := []SubscriptionPlan{}
|
|
for rows.Next() {
|
|
var p SubscriptionPlan
|
|
if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.ProductID, &p.Interval, &p.IntervalCount, &p.Price, &p.Currency, &p.TrialDays, &p.SetupFee, &p.Status, &p.CreatedAt); err != nil {
|
|
continue
|
|
}
|
|
plans = append(plans, p)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"plans": plans,
|
|
"total": len(plans),
|
|
})
|
|
}
|
|
|
|
func (h *SubscriptionHandler) CreatePlan(w http.ResponseWriter, r *http.Request) {
|
|
var req SubscriptionPlan
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request")
|
|
return
|
|
}
|
|
|
|
var id string
|
|
err := h.DB.QueryRow(`
|
|
INSERT INTO boc_subscription_plans (name, description, product_id, interval, interval_count, price, currency, trial_days, setup_fee)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
RETURNING id
|
|
`, req.Name, req.Description, req.ProductID, req.Interval, req.IntervalCount, req.Price, req.Currency, req.TrialDays, req.SetupFee).Scan(&id)
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create plan")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
|
"id": id,
|
|
"message": "Subscription plan created",
|
|
})
|
|
}
|
|
|
|
func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.Request) {
|
|
status := r.URL.Query().Get("status")
|
|
if status == "" {
|
|
status = "all"
|
|
}
|
|
|
|
var query string
|
|
var args []interface{}
|
|
if status == "all" {
|
|
query = `SELECT id, customer_id, plan_id, status, start_date, end_date, trial_end, current_period_start, current_period_end, price, currency, created_at FROM boc_subscriptions ORDER BY created_at DESC LIMIT 100`
|
|
} else {
|
|
query = `SELECT id, customer_id, plan_id, status, start_date, end_date, trial_end, current_period_start, current_period_end, price, currency, created_at FROM boc_subscriptions WHERE status = $1 ORDER BY created_at DESC LIMIT 100`
|
|
args = append(args, status)
|
|
}
|
|
|
|
rows, err := h.DB.Query(query, args...)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
subs := []Subscription{}
|
|
for rows.Next() {
|
|
var s Subscription
|
|
if err := rows.Scan(&s.ID, &s.CustomerID, &s.PlanID, &s.Status, &s.StartDate, &s.EndDate, &s.TrialEnd, &s.CurrentPeriodStart, &s.CurrentPeriodEnd, &s.Price, &s.Currency, &s.CreatedAt); err != nil {
|
|
continue
|
|
}
|
|
subs = append(subs, s)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"subscriptions": subs,
|
|
"total": len(subs),
|
|
})
|
|
}
|
|
|
|
func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
CustomerID string `json:"customer_id"`
|
|
PlanID string `json:"plan_id"`
|
|
StartDate time.Time `json:"start_date"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request")
|
|
return
|
|
}
|
|
|
|
// Get plan details
|
|
var planPrice float64
|
|
var planCurrency string
|
|
var trialDays int
|
|
err := h.DB.QueryRow(`SELECT price, currency, trial_days FROM boc_subscription_plans WHERE id = $1`, req.PlanID).Scan(&planPrice, &planCurrency, &trialDays)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "plan not found")
|
|
return
|
|
}
|
|
|
|
// Calculate dates
|
|
var trialEnd, periodStart, periodEnd *time.Time
|
|
start := req.StartDate
|
|
periodStart = &start
|
|
|
|
if trialDays > 0 {
|
|
t := start.AddDate(0, 0, trialDays)
|
|
trialEnd = &t
|
|
periodStart = trialEnd
|
|
}
|
|
|
|
pe := periodStart.AddDate(0, 1, 0) // Monthly default
|
|
periodEnd = &pe
|
|
|
|
var id string
|
|
err = h.DB.QueryRow(`
|
|
INSERT INTO boc_subscriptions (customer_id, plan_id, status, start_date, end_date, trial_end, current_period_start, current_period_end, price, currency)
|
|
VALUES ($1, $2, 'active', $3, NULL, $4, $5, $6, $7, $8)
|
|
RETURNING id
|
|
`, req.CustomerID, req.PlanID, start, trialEnd, periodStart, periodEnd, planPrice, planCurrency).Scan(&id)
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create subscription")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
|
"id": id,
|
|
"message": "Subscription created",
|
|
})
|
|
}
|
|
|
|
func (h *SubscriptionHandler) GenerateRecurringInvoices(w http.ResponseWriter, r *http.Request) {
|
|
// Find subscriptions with period ending soon
|
|
rows, err := h.DB.Query(`
|
|
SELECT s.id, s.customer_id, s.plan_id, s.price, s.currency, s.current_period_end
|
|
FROM boc_subscriptions s
|
|
WHERE s.status = 'active'
|
|
AND s.current_period_end <= NOW() + INTERVAL '7 days'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM boc_recurring_invoices ri
|
|
WHERE ri.subscription_id = s.id
|
|
AND ri.scheduled_date = s.current_period_end
|
|
)
|
|
`)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
generated := 0
|
|
for rows.Next() {
|
|
var subID, customerID, planID string
|
|
var price float64
|
|
var currency string
|
|
var periodEnd time.Time
|
|
if err := rows.Scan(&subID, &customerID, &planID, &price, ¤cy, &periodEnd); err != nil {
|
|
continue
|
|
}
|
|
|
|
invoiceNumber := fmt.Sprintf("SUB-%d-%s", time.Now().Unix(), subID[:8])
|
|
_, err = h.DB.Exec(`
|
|
INSERT INTO boc_recurring_invoices (customer_id, subscription_id, plan_id, invoice_number, amount, currency, scheduled_date)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
`, customerID, subID, planID, invoiceNumber, price, currency, periodEnd)
|
|
if err == nil {
|
|
generated++
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"generated": generated,
|
|
"message": fmt.Sprintf("Generated %d recurring invoices", generated),
|
|
})
|
|
}
|
|
|
|
func (h *SubscriptionHandler) ListRecurringInvoices(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := h.DB.Query(`
|
|
SELECT id, customer_id, subscription_id, plan_id, invoice_number, amount, currency, status, scheduled_date, generated_at, sent_at
|
|
FROM boc_recurring_invoices
|
|
ORDER BY scheduled_date DESC
|
|
LIMIT 100
|
|
`)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "database error")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
invoices := []map[string]interface{}{}
|
|
for rows.Next() {
|
|
var id, customerID, subID, planID, invNumber, status, currency string
|
|
var amount float64
|
|
var scheduledDate time.Time
|
|
var generatedAt, sentAt *time.Time
|
|
if err := rows.Scan(&id, &customerID, &subID, &planID, &invNumber, &amount, ¤cy, &status, &scheduledDate, &generatedAt, &sentAt); err != nil {
|
|
continue
|
|
}
|
|
invoices = append(invoices, map[string]interface{}{
|
|
"id": id,
|
|
"customer_id": customerID,
|
|
"subscription_id": subID,
|
|
"invoice_number": invNumber,
|
|
"amount": amount,
|
|
"currency": currency,
|
|
"status": status,
|
|
"scheduled_date": scheduledDate,
|
|
"generated_at": generatedAt,
|
|
"sent_at": sentAt,
|
|
})
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"invoices": invoices,
|
|
"total": len(invoices),
|
|
})
|
|
}
|