From 245d4e2977879b1f0f56f5d1c16dafd5d32d7875 Mon Sep 17 00:00:00 2001 From: aserven Date: Fri, 9 May 2025 11:57:09 +0200 Subject: [PATCH 1/2] Add subscription on top of credits --- cmd/server/templates/dashboard.html | 17 ++++++--- routes/handler.go | 57 +++++++++++++++++++++-------- routes/service.go | 9 +++++ 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/cmd/server/templates/dashboard.html b/cmd/server/templates/dashboard.html index 36484bd..2279c3d 100644 --- a/cmd/server/templates/dashboard.html +++ b/cmd/server/templates/dashboard.html @@ -192,9 +192,9 @@

Daily Earnings

- + @@ -245,6 +245,7 @@

Daily Earnings

@@ -301,6 +302,8 @@

Daily Earnings

{{if and (eq .Type "credit") (eq .Credits 1)}} One-time payment + {{else if eq .Type "subscription"}} + Subscription {{else}} {{/* Capitalize first letter of Type for display */}} {{$typeDisplay := .Type}} @@ -632,12 +635,12 @@

No links yet

const linkForm = document.getElementById('link-form'); function toggleCreditsField() { - if (typeSelect.value === 'one-time') { + if (typeSelect.value === 'one-time' || typeSelect.value === 'subscription') { creditsInputContainer.style.display = 'none'; - creditsInput.value = '1'; - } else { + creditsInput.value = '1'; // Set to 1 for one-time and subscription + } else { // 'credit' creditsInputContainer.style.display = 'inline-flex'; // Explicitly set to its styled display type - // Optionally, you might want to clear creditsInput.value or set a default + // Optionally, you might want to clear creditsInput.value or set a default for 'credit' if not 1 } } @@ -657,7 +660,9 @@

No links yet

// The creditsInput.value is already '1' due to toggleCreditsField document.getElementById('type-select').value = 'credit'; } + // No change needed for 'subscription' type, it will submit as 'subscription' // The form will now submit with type='credit' and credits='1' if 'one-time' was chosen + // and type='subscription' and credits='1' if 'subscription' was chosen }); } }); diff --git a/routes/handler.go b/routes/handler.go index 581a67c..35510f6 100644 --- a/routes/handler.go +++ b/routes/handler.go @@ -12,6 +12,7 @@ import ( "net/http/httputil" "net/url" "strconv" + "time" "github.com/gin-gonic/gin" @@ -196,26 +197,50 @@ func (h *PaidRouteHandler) tryExistingPayment(gCtx *gin.Context, route *PaidRout "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID, "creditsUsed", existingPurchase.CreditsUsed, "creditsAvailable", existingPurchase.CreditsAvailable) - if existingPurchase.CreditsUsed >= existingPurchase.CreditsAvailable { - h.logger.Info("Existing purchase (via header) has no credits left. Proceeding to new payment.", + switch existingPurchase.Type { + case "credit": + if existingPurchase.CreditsUsed >= existingPurchase.CreditsAvailable { + h.logger.Info("Existing 'credit' purchase (via header) has no credits left. Proceeding to new payment.", + "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID) + return false, true // No credits left, proceed to new payment. + } + + // Credits are available for 'credit' type, attempt to use one. + h.logger.Debug("Existing 'credit' purchase has available credits. Attempting to use one credit.", + "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID) + + errIncrement := h.purchaseService.IncrementCreditsUsed(gCtx.Request.Context(), existingPurchase.ID) + if errIncrement != nil { + h.logger.Error("Failed to increment credits_used for 'credit' purchase. Proceeding to new payment.", + "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID, "error", errIncrement) + return false, true // Failed to use credit, proceed to new payment. + } + h.logger.Info("Successfully used a credit from existing 'credit' purchase via payment header.", "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID) - return false, true // No credits left, proceed to new payment. - } - // Credits are available, attempt to use one. - h.logger.Debug("Existing purchase has available credits. Attempting to use one credit.", - "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID) + case "subscription": + // For subscription, check if the subscription period is still valid (1 month from CreatedAt) + expiryDate := existingPurchase.CreatedAt.AddDate(0, 1, 0) // Add 1 month + currentTime := time.Now() - errIncrement := h.purchaseService.IncrementCreditsUsed(gCtx.Request.Context(), existingPurchase.ID) - if errIncrement != nil { - h.logger.Error("Failed to increment credits_used for existing purchase. Proceeding to new payment.", - "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID, "error", errIncrement) - return false, true // Failed to use credit, proceed to new payment. - } + if currentTime.After(expiryDate) { + h.logger.Info("Existing 'subscription' purchase (via header) has expired. Proceeding to new payment.", + "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID, + "createdAt", existingPurchase.CreatedAt, "expiryDate", expiryDate) + return false, true // Subscription expired, proceed to new payment. + } - // Successfully used a credit from an existing purchase. - h.logger.Info("Successfully used a credit from existing purchase via payment header.", - "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID) + // Subscription is active. No credit decrement needed for time-based access. + h.logger.Info("Successfully validated active 'subscription' via payment header.", + "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID, "expiryDate", expiryDate) + + default: + // Unknown or unhandled purchase type with an existing payment header. + // This case should ideally not be reached if route creation is validated properly. + h.logger.Warn("Encountered unknown purchase type with existing payment header. Proceeding to new payment.", + "shortCode", route.ShortCode, "purchaseID", existingPurchase.ID, "purchaseType", existingPurchase.Type) + return false, true // Proceed to new payment as a safe default. + } // Increment overall access count for the route. if err := h.paidRouteService.IncrementAccessCount(gCtx.Request.Context(), route.ShortCode); err != nil { diff --git a/routes/service.go b/routes/service.go index 7ff4e67..03cc57d 100644 --- a/routes/service.go +++ b/routes/service.go @@ -58,6 +58,15 @@ func (s *PaidRouteService) CreatePaidRoute(ctx context.Context, targetURL, // Convert to integer (USDC * 10^6) priceInt := uint64(priceFloat * 1000000) + // 4. Validate Route Type + allowedTypes := map[string]bool{ + "credit": true, + "subscription": true, + } + if !allowedTypes[routeType] { + return nil, fmt.Errorf("invalid route type specified: %s", routeType) + } + // Create and Save Route (short code will be generated in the store) route := &PaidRoute{ TargetURL: targetURL, From e656e513b7903f2ac0f964ee4172aba66ca67866 Mon Sep 17 00:00:00 2001 From: aserven Date: Mon, 12 May 2025 16:43:59 +0200 Subject: [PATCH 2/2] Remove comment --- store/sqlc/migrations/000003_add_credits_system.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/sqlc/migrations/000003_add_credits_system.up.sql b/store/sqlc/migrations/000003_add_credits_system.up.sql index 490a799..60cf78b 100644 --- a/store/sqlc/migrations/000003_add_credits_system.up.sql +++ b/store/sqlc/migrations/000003_add_credits_system.up.sql @@ -8,7 +8,7 @@ ALTER TABLE purchases ADD COLUMN type VARCHAR(50) NOT NULL DEFAULT 'credit', -- Assuming default matches routes ADD COLUMN credits_available INTEGER NOT NULL DEFAULT 0, -- Default 0, will be set on creation ADD COLUMN credits_used INTEGER NOT NULL DEFAULT 0, -ADD COLUMN payment_header TEXT; -- Or BYTEA if storing binary header data +ADD COLUMN payment_header TEXT; -- Create the new composite index on payment_header and paid_route_id CREATE INDEX IF NOT EXISTS idx_purchases_payment_header_route_id ON purchases (payment_header, paid_route_id);