mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
POST /v1/billing/subscribe/card takes an optional coupon code. The coupon is a models/coupon row (Filter "plan", Type percent) in the reserved platform namespace "admin", beside the platform promo. It must be enabled, inside [StartDate, EndDate], under Limit and cover the plan; an org and a card each redeem a code once (models/couponredemption rows), and the use is reserved under the code's lock before the card is charged and given back on a decline. Any refusal is a 400 or 409 before the card is touched. The percent comes off the first charge only, through engine.Discounted, the same arithmetic the first invoice uses, so charge == AmountDue. The first invoice names the coupon; the subscription never carries it, so renewals bill the catalog price (less only a promo the row carries). GET /v1/billing/plans?coupon=CODE quotes couponCode, couponPercent, couponFirstCents and couponEnds on covered paid rows and couponError (unknown, expired, exhausted, not_applicable) on the rest. Without the parameter the response is unchanged. SuperAdmin: GET/PUT /v1/platform/coupons/:code (RequirePlatformAdmin), audited as coupon.created/coupon.updated billing events naming the actor. middleware.Actor is the one actor-naming rule, moved from billing's minter. Co-authored-by: Hanzo Dev <[email protected]>
175 lines
4.2 KiB
Go
175 lines
4.2 KiB
Go
package coupon
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/commerce/datastore"
|
|
"github.com/hanzoai/commerce/log"
|
|
"github.com/hanzoai/commerce/models/mixin"
|
|
"github.com/hanzoai/commerce/util/hashid"
|
|
"github.com/hanzoai/commerce/util/timeutil"
|
|
"github.com/hanzoai/orm"
|
|
)
|
|
|
|
func init() { orm.Register[Coupon]("coupon") }
|
|
|
|
type Type string
|
|
|
|
const (
|
|
Flat Type = "flat"
|
|
Percent = "percent"
|
|
FreeShipping = "free-shipping"
|
|
FreeItem = "free-item"
|
|
)
|
|
|
|
var Types = []Type{Flat, Percent, FreeShipping}
|
|
|
|
// PlanFilter marks a coupon that discounts a subscription PLAN purchase rather
|
|
// than a cart: the first period of a plan bought with its code.
|
|
const PlanFilter = "plan"
|
|
|
|
type Coupon struct {
|
|
mixin.Model[Coupon]
|
|
|
|
Name string `json:"name"`
|
|
|
|
// Possible values: flat, percent, free_shipping.
|
|
Type Type `json:"type"`
|
|
|
|
// Coupon code (must be unique).
|
|
Code_ string `json:"code" datastore:"Code"`
|
|
RawCode string `json:"-" datastore:"-"`
|
|
|
|
// Indicates whether or not the Code is dynamically checked (for something like user-generated coupons)
|
|
Dynamic bool `json:"dynamic"`
|
|
|
|
CampaignId string `json:"campaignId,omitempty"`
|
|
ReferrerId string `json:"referrerId,omitempty"`
|
|
|
|
// Range in which coupon is valid
|
|
StartDate time.Time `json:"startDate"`
|
|
EndDate time.Time `json:"endDate"`
|
|
|
|
// Possible values: order, product, plan (PlanFilter).
|
|
Filter string `json:"filter"`
|
|
|
|
// Indicates whether this coupon may be applied once or more than once at checkout.
|
|
Once bool `json:"once"`
|
|
|
|
// The number of times this coupon can be used before it is used up and useless. 0 = unlimited
|
|
Limit int `json:"limit"`
|
|
|
|
// Product id for product-specific coupons.
|
|
ProductId string `json:"productId,omitempty"`
|
|
|
|
// Plans are the plan slugs a plan coupon applies to, beside ProductId. Both
|
|
// empty means every paid plan.
|
|
Plans []string `json:"plans,omitempty"`
|
|
|
|
// Whether coupon is valid.
|
|
Enabled bool `json:"enabled" orm:"default:true"`
|
|
|
|
// Coupon amount. $5 should be 500 (prices in basic currency unit, like cents). 10% should be 10.
|
|
// TODO: This needs to be currency.Cents in Hanzo.
|
|
Amount int `json:"amount"`
|
|
|
|
// Number of times coupon was redeemed.
|
|
Used int `json:"used"`
|
|
|
|
// Free product with coupon
|
|
FreeProductId string `json:"freeProductId"`
|
|
FreeVariantId string `json:"freeVariantId"`
|
|
FreeQuantity int `json:"freeQuantity"`
|
|
|
|
// List of buyer email addresses who have redeemed coupon.
|
|
// Buyers []string `json:"buyers"`
|
|
}
|
|
|
|
func (co *Coupon) Load(ps []datastore.Property) (err error) {
|
|
// Load supported properties
|
|
if err = datastore.LoadStruct(co, ps); err != nil {
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (co *Coupon) Save() (ps []datastore.Property, err error) {
|
|
|
|
co.Code_ = strings.ToUpper(co.Code_)
|
|
|
|
// Save properties
|
|
return datastore.SaveStruct(co)
|
|
}
|
|
|
|
func (c Coupon) Code() string {
|
|
if c.RawCode != "" && c.RawCode != c.Code_ {
|
|
return c.RawCode
|
|
} else {
|
|
return c.Code_
|
|
}
|
|
}
|
|
|
|
func (c Coupon) DynamicCode() string {
|
|
if c.RawCode == c.Code_ {
|
|
return ""
|
|
}
|
|
|
|
return c.RawCode
|
|
}
|
|
|
|
func (c *Coupon) CodeFromId(uniqueid string) string {
|
|
cid := c.Key()
|
|
uid, err := hashid.DecodeKey(c.Context(), uniqueid)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Normal kind id for coupon is 3, this is 3333 to prevent accidental
|
|
// decoding as normal hashid
|
|
return hashid.Encode(3333, int(cid.IntID()), int(uid.IntID()))
|
|
}
|
|
|
|
func (c Coupon) ValidFor(t time.Time) bool {
|
|
if !c.Enabled {
|
|
log.Warn("Coupon Not Enabled", c.Context())
|
|
return false // currently active, no need to check?
|
|
}
|
|
|
|
if !timeutil.IsZero(c.StartDate) && c.StartDate.After(t) {
|
|
log.Warn("Coupon not yet Usable: %v > %v", c.StartDate.Unix(), t, c.Context())
|
|
return false
|
|
}
|
|
|
|
if !timeutil.IsZero(c.EndDate) && c.EndDate.Before(t) {
|
|
log.Warn("Coupon is Expired: %v < %v", c.EndDate.Unix(), t, c.Context())
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (c Coupon) ItemId() string {
|
|
if c.ProductId != "" {
|
|
return c.ProductId
|
|
}
|
|
if c.FreeProductId != "" {
|
|
return c.FreeProductId
|
|
}
|
|
if c.FreeVariantId != "" {
|
|
return c.FreeProductId
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func New(db *datastore.Datastore) *Coupon {
|
|
c := new(Coupon)
|
|
c.Init(db)
|
|
return c
|
|
}
|
|
|
|
func Query(db *datastore.Datastore) datastore.Query {
|
|
return db.Query("coupon")
|
|
}
|