feat(boc): v1.0 - Complete Business Operations Center
- 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
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
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),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user