Source Code

;; Subscription Service - Recurring payment platform
;; Built for Stacks Builder Challenge by Marcus David

(define-constant contract-owner tx-sender)
(define-constant err-not-found (err u101))
(define-constant err-unauthorized (err u102))
(define-constant err-already-subscribed (err u103))
(define-constant err-payment-too-early (err u104))

(define-data-var grace-period uint u144) ;; ~1 day in blocks
(define-data-var next-plan-id uint u1)

(define-map plans
  uint
  {
    provider: principal,
    name: (string-ascii 50),
    price: uint,
    billing-period: uint,
    active: bool
  }
)

(define-map subscriptions
  { plan-id: uint, subscriber: principal }
  {
    start-block: uint,
    last-payment-block: uint,
    next-payment-due: uint,
    active: bool
  }
)

(define-read-only (get-plan (plan-id uint))
  (map-get? plans plan-id)
)

(define-read-only (get-subscription (plan-id uint) (subscriber principal))
  (map-get? subscriptions { plan-id: plan-id, subscriber: subscriber })
)

(define-public (create-plan (name (string-ascii 50)) (price uint) (billing-period uint))
  (let ((plan-id (var-get next-plan-id)))
    (map-set plans plan-id {
      provider: tx-sender,
      name: name,
      price: price,
      billing-period: billing-period,
      active: true
    })
    (var-set next-plan-id (+ plan-id u1))
    (ok plan-id)
  )
)

(define-public (subscribe (plan-id uint))
  (match (map-get? plans plan-id)
    plan
      (begin
        (asserts! (get active plan) err-not-found)
        (asserts! (is-none (map-get? subscriptions { plan-id: plan-id, subscriber: tx-sender })) 
                  err-already-subscribed)
        
        (try! (stx-transfer-memo? (get price plan) tx-sender (get provider plan) 0x737562736372697074696f6e207061796d656e74))
        (map-set subscriptions { plan-id: plan-id, subscriber: tx-sender } {
          start-block: stacks-block-height,
          last-payment-block: stacks-block-height,
          next-payment-due: (+ stacks-block-height (get billing-period plan)),
          active: true
        })
        (ok true)
      )
    err-not-found
  )
)

(define-public (renew-subscription (plan-id uint))
  (match (map-get? plans plan-id)
    plan
      (match (map-get? subscriptions { plan-id: plan-id, subscriber: tx-sender })
        subscription
          (begin
            (asserts! (get active subscription) err-not-found)
            (asserts! (>= stacks-block-height (get next-payment-due subscription)) err-payment-too-early)
            
            (try! (stx-transfer-memo? (get price plan) tx-sender (get provider plan) 0x737562736372697074696f6e2072656e6577616c))
            (map-set subscriptions { plan-id: plan-id, subscriber: tx-sender }
              (merge subscription {
                last-payment-block: stacks-block-height,
                next-payment-due: (+ stacks-block-height (get billing-period plan))
              })
            )
            (ok true)
          )
        err-not-found
      )
    err-not-found
  )
)

(define-public (cancel-subscription (plan-id uint))
  (match (map-get? subscriptions { plan-id: plan-id, subscriber: tx-sender })
    subscription
      (begin
        (map-set subscriptions { plan-id: plan-id, subscriber: tx-sender }
          (merge subscription { active: false })
        )
        (ok true)
      )
    err-not-found
  )
)

(define-read-only (is-subscription-active (plan-id uint) (subscriber principal))
  (match (map-get? subscriptions { plan-id: plan-id, subscriber: subscriber })
    subscription
      (ok (and
        (get active subscription)
        (<= stacks-block-height (+ (get next-payment-due subscription) (var-get grace-period)))
      ))
    (ok false)
  )
)

Functions (7)

FunctionAccessArgs
get-planread-onlyplan-id: uint
get-subscriptionread-onlyplan-id: uint, subscriber: principal
create-planpublicname: (string-ascii 50
subscribepublicplan-id: uint
renew-subscriptionpublicplan-id: uint
cancel-subscriptionpublicplan-id: uint
is-subscription-activeread-onlyplan-id: uint, subscriber: principal