mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 08:01:24 +00:00
225 lines
9.6 KiB
Go
225 lines
9.6 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// DiscountType is the kind of discount a code grants. Exactly one value field
|
|
// on the code is populated per type (see the DB CHECK in 000048).
|
|
type DiscountType string
|
|
|
|
const (
|
|
DiscountTypePercent DiscountType = "percent"
|
|
DiscountTypeFixed DiscountType = "fixed"
|
|
DiscountTypeTrialExtension DiscountType = "trial_extension"
|
|
)
|
|
|
|
// DiscountDuration mirrors Stripe coupon duration for money discounts.
|
|
type DiscountDuration string
|
|
|
|
const (
|
|
DiscountDurationOnce DiscountDuration = "once"
|
|
DiscountDurationRepeating DiscountDuration = "repeating"
|
|
DiscountDurationForever DiscountDuration = "forever"
|
|
)
|
|
|
|
type DiscountCodeStatus string
|
|
|
|
const (
|
|
DiscountCodeStatusActive DiscountCodeStatus = "active"
|
|
DiscountCodeStatusDisabled DiscountCodeStatus = "disabled"
|
|
DiscountCodeStatusExpired DiscountCodeStatus = "expired"
|
|
)
|
|
|
|
type DiscountRedemptionStatus string
|
|
|
|
const (
|
|
DiscountRedemptionStatusPending DiscountRedemptionStatus = "pending"
|
|
DiscountRedemptionStatusApplied DiscountRedemptionStatus = "applied"
|
|
DiscountRedemptionStatusCanceled DiscountRedemptionStatus = "canceled"
|
|
)
|
|
|
|
// IsMoney reports whether the discount reduces the charged amount (vs granting
|
|
// extra trial days).
|
|
func (t DiscountType) IsMoney() bool {
|
|
return t == DiscountTypePercent || t == DiscountTypeFixed
|
|
}
|
|
|
|
// DiscountCode is an admin-managed promo code.
|
|
type DiscountCode struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Code string `json:"code"`
|
|
Description string `json:"description"`
|
|
Type DiscountType `json:"type"`
|
|
|
|
PercentOff *int `json:"percent_off,omitempty"`
|
|
AmountOff *float64 `json:"amount_off,omitempty"`
|
|
Currency *string `json:"currency,omitempty"`
|
|
TrialExtensionDays *int `json:"trial_extension_days,omitempty"`
|
|
|
|
Duration DiscountDuration `json:"duration"`
|
|
DurationInMonths *int `json:"duration_in_months,omitempty"`
|
|
|
|
MaxRedemptions *int `json:"max_redemptions,omitempty"`
|
|
TimesRedeemed int `json:"times_redeemed"`
|
|
PerAccountLimit int `json:"per_account_limit"`
|
|
|
|
AppliesToAllPlans bool `json:"applies_to_all_plans"`
|
|
PlanIDs []uuid.UUID `json:"plan_ids"`
|
|
|
|
Status DiscountCodeStatus `json:"status"`
|
|
StartsAt *time.Time `json:"starts_at,omitempty"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
|
|
CreatedBy *uuid.UUID `json:"created_by,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// DiscountRedemption records a single redemption of a code by an organization.
|
|
type DiscountRedemption struct {
|
|
ID uuid.UUID `json:"id"`
|
|
DiscountCodeID uuid.UUID `json:"discount_code_id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
RedeemedBy *uuid.UUID `json:"redeemed_by,omitempty"`
|
|
SubscriptionID *uuid.UUID `json:"subscription_id,omitempty"`
|
|
PlanID *uuid.UUID `json:"plan_id,omitempty"`
|
|
StripeCouponID *string `json:"stripe_coupon_id,omitempty"`
|
|
StripeCheckoutSessionID *string `json:"stripe_checkout_session_id,omitempty"`
|
|
|
|
Type DiscountType `json:"type"`
|
|
PercentOff *int `json:"percent_off,omitempty"`
|
|
AmountOff *float64 `json:"amount_off,omitempty"`
|
|
Currency *string `json:"currency,omitempty"`
|
|
TrialExtensionDays *int `json:"trial_extension_days,omitempty"`
|
|
|
|
Status DiscountRedemptionStatus `json:"status"`
|
|
RedeemedAt time.Time `json:"redeemed_at"`
|
|
AppliedAt *time.Time `json:"applied_at,omitempty"`
|
|
|
|
// Joined (populated by admin list-by-code).
|
|
Code string `json:"code,omitempty"`
|
|
}
|
|
|
|
// CreateDiscountCodeRequest is the admin create payload.
|
|
type CreateDiscountCodeRequest struct {
|
|
Code string `json:"code" binding:"required"`
|
|
Description string `json:"description"`
|
|
Type DiscountType `json:"type" binding:"required"`
|
|
PercentOff *int `json:"percent_off,omitempty"`
|
|
AmountOff *float64 `json:"amount_off,omitempty"`
|
|
Currency *string `json:"currency,omitempty"`
|
|
TrialExtensionDays *int `json:"trial_extension_days,omitempty"`
|
|
Duration DiscountDuration `json:"duration,omitempty"`
|
|
DurationInMonths *int `json:"duration_in_months,omitempty"`
|
|
MaxRedemptions *int `json:"max_redemptions,omitempty"`
|
|
PerAccountLimit *int `json:"per_account_limit,omitempty"`
|
|
AppliesToAllPlans bool `json:"applies_to_all_plans"`
|
|
PlanIDs []uuid.UUID `json:"plan_ids,omitempty"`
|
|
Status DiscountCodeStatus `json:"status,omitempty"`
|
|
StartsAt *time.Time `json:"starts_at,omitempty"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// UpdateDiscountCodeRequest is the admin partial-update payload. The discount
|
|
// `type` is immutable; recreate the code to change kinds.
|
|
type UpdateDiscountCodeRequest struct {
|
|
Description *string `json:"description,omitempty"`
|
|
PercentOff *int `json:"percent_off,omitempty"`
|
|
AmountOff *float64 `json:"amount_off,omitempty"`
|
|
Currency *string `json:"currency,omitempty"`
|
|
TrialExtensionDays *int `json:"trial_extension_days,omitempty"`
|
|
Duration *DiscountDuration `json:"duration,omitempty"`
|
|
DurationInMonths *int `json:"duration_in_months,omitempty"`
|
|
MaxRedemptions *int `json:"max_redemptions,omitempty"`
|
|
PerAccountLimit *int `json:"per_account_limit,omitempty"`
|
|
AppliesToAllPlans *bool `json:"applies_to_all_plans,omitempty"`
|
|
PlanIDs *[]uuid.UUID `json:"plan_ids,omitempty"`
|
|
Status *DiscountCodeStatus `json:"status,omitempty"`
|
|
StartsAt *time.Time `json:"starts_at,omitempty"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// AdminDiscountSearch filters the admin discount list. Note the free-text key
|
|
// is "search" (not "q") to preserve the existing endpoint contract.
|
|
type AdminDiscountSearch struct {
|
|
Search string `form:"search"`
|
|
Status string `form:"status"` // active|disabled|expired|all
|
|
Type string `form:"type"` // percent|fixed|trial_extension
|
|
Duration string `form:"duration"` // once|repeating|forever
|
|
|
|
// Plan eligibility
|
|
PlanScope string `form:"plan_scope"` // all|specific
|
|
PlanID *uuid.UUID `form:"plan_id"`
|
|
|
|
// Flag / relationship existence
|
|
HasRedemptions bool `form:"has_redemptions"`
|
|
HasMaxRedemptions bool `form:"has_max_redemptions"`
|
|
Exhausted bool `form:"exhausted"`
|
|
HasExpiry bool `form:"has_expiry"`
|
|
|
|
// Count ranges
|
|
TimesRedeemedMin *int `form:"times_redeemed_min"`
|
|
TimesRedeemedMax *int `form:"times_redeemed_max"`
|
|
PercentOffMin *int `form:"percent_off_min"`
|
|
PercentOffMax *int `form:"percent_off_max"`
|
|
|
|
// Date ranges (YYYY-MM-DD, UTC)
|
|
CreatedWithin int `form:"created_within"` // days; 0 = any
|
|
CreatedAfter *time.Time `form:"created_after" time_format:"2006-01-02" time_utc:"true"`
|
|
CreatedBefore *time.Time `form:"created_before" time_format:"2006-01-02" time_utc:"true"`
|
|
StartsAfter *time.Time `form:"starts_after" time_format:"2006-01-02" time_utc:"true"`
|
|
StartsBefore *time.Time `form:"starts_before" time_format:"2006-01-02" time_utc:"true"`
|
|
ExpiresAfter *time.Time `form:"expires_after" time_format:"2006-01-02" time_utc:"true"`
|
|
ExpiresBefore *time.Time `form:"expires_before" time_format:"2006-01-02" time_utc:"true"`
|
|
|
|
// Offset is decoded from the opaque ?cursor token by the handler (the wire
|
|
// cursor stays opaque, like every other list), not bound from the query.
|
|
Offset int `form:"-"`
|
|
Limit int `form:"limit"`
|
|
SortBy string `form:"sort_by"` // created_at|code|status|times_redeemed|expires_at|starts_at|updated_at
|
|
SortDesc bool `form:"sort_desc"`
|
|
}
|
|
|
|
// AdminDiscountsResult is the paginated admin discount list envelope.
|
|
type AdminDiscountsResult struct {
|
|
Data []DiscountCode `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// AdminDiscountRedemptionsResult is the paginated redemptions envelope.
|
|
type AdminDiscountRedemptionsResult struct {
|
|
Data []DiscountRedemption `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// ValidateDiscountRequest is the customer pre-checkout validation payload.
|
|
type ValidateDiscountRequest struct {
|
|
Code string `json:"code" binding:"required"`
|
|
PlanID *uuid.UUID `json:"plan_id,omitempty"`
|
|
}
|
|
|
|
// DiscountPreview is the customer-facing validation result. When invalid,
|
|
// Reason explains why; the discount fields are only populated when Valid.
|
|
type DiscountPreview struct {
|
|
Valid bool `json:"valid"`
|
|
Reason string `json:"reason,omitempty"`
|
|
|
|
Code string `json:"code,omitempty"`
|
|
Type DiscountType `json:"type,omitempty"`
|
|
PercentOff *int `json:"percent_off,omitempty"`
|
|
AmountOff *float64 `json:"amount_off,omitempty"`
|
|
Currency *string `json:"currency,omitempty"`
|
|
TrialExtensionDays *int `json:"trial_extension_days,omitempty"`
|
|
Duration DiscountDuration `json:"duration,omitempty"`
|
|
DurationInMonths *int `json:"duration_in_months,omitempty"`
|
|
|
|
// Plan-specific computation, only when a plan_id is supplied and the code is
|
|
// a money discount.
|
|
OriginalAmount *float64 `json:"original_amount,omitempty"`
|
|
DiscountedAmount *float64 `json:"discounted_amount,omitempty"`
|
|
SavingsAmount *float64 `json:"savings_amount,omitempty"`
|
|
}
|