{{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,
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);