Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions cmd/server/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ <h3>Daily Earnings</h3>
<select id="type-select" name="type" required>
<option value="credit">Credit</option>
<option value="one-time" selected>One-time payment</option>
<!-- Add other types here if needed, e.g., <option value="subscription">Subscription</option> -->
<option value="subscription">Subscription</option>
</select>
<span class="tooltip" data-tooltip="ONE-TIME PAYMENT: User pays once for a single access.&#xA;CREDIT: User pays once for a set number of accesses.&#xA; (To use credits for subsequent visits, the client must resend the `X-Payment` header from the original successful payment.)">
<span class="tooltip" data-tooltip="ONE-TIME PAYMENT: User pays once for a single access.&#xA;CREDIT: User pays once for a set number of accesses.&#xA;SUBSCRIPTION: User pays monthly for continued access.&#xA; (To use credits for subsequent visits for credit and subscription models, the client must resend the `X-Payment` header from the original successful payment.)">
<i data-lucide="help-circle" width="16" height="16"></i>
</span>

Expand Down Expand Up @@ -245,6 +245,7 @@ <h3>Daily Earnings</h3>
<select id="type-filter" class="filter-select">
<option value="All">All</option>
<option value="credit">Credit</option>
<option value="subscription">Subscription</option>
<!-- Add other types here if they become available -->
</select>
</div>
Expand Down Expand Up @@ -301,6 +302,8 @@ <h3>Daily Earnings</h3>
<td>
{{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}}
Expand Down Expand Up @@ -632,12 +635,12 @@ <h3>No links yet</h3>
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
}
}

Expand All @@ -657,7 +660,9 @@ <h3>No links yet</h3>
// 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
});
}
});
Expand Down
57 changes: 41 additions & 16 deletions routes/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/http/httputil"
"net/url"
"strconv"
"time"

"github.com/gin-gonic/gin"

Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions routes/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion store/sqlc/migrations/000003_add_credits_system.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);