Source Code

;; CircleCare Expense Factory - Clarity 4
;; Creates and manages support groups (nests) on Stacks
;; Uses stacks-block-time for timestamp tracking

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-OWNER-ONLY (err u100))
(define-constant ERR-INVALID-NAME (err u101))
(define-constant ERR-INVALID-NICKNAME (err u102))
(define-constant ERR-MAX-GROUPS-REACHED (err u103))
(define-constant ERR-INSUFFICIENT-FEE (err u104))
(define-constant ERR-GROUP-NOT-FOUND (err u105))
(define-constant ERR-UNAUTHORIZED (err u106))
(define-constant ERR-GROUP-INACTIVE (err u107))
(define-constant ERR-WITHDRAWAL-FAILED (err u108))

;; Data Variables
(define-data-var creation-fee uint u0) ;; Fee in microSTX (1 STX = 1,000,000 microSTX)
(define-data-var max-groups-per-user uint u10)
(define-data-var next-group-id uint u1)
(define-data-var total-fees-collected uint u0)

;; Data Maps
(define-map group-info
  uint
  {
    name: (string-ascii 50),
    creator: principal,
    created-at: uint, ;; Unix timestamp using stacks-block-time
    active: bool,
    treasury-contract: (optional principal)
  }
)

;; User's groups: principal => list of group IDs
(define-map user-groups principal (list 100 uint))

;; All group IDs for iteration
(define-map all-groups-index uint uint)
(define-data-var all-groups-count uint u0)

;; Public Functions

;; Create a new support group (nest) and automatically initialize in treasury
(define-public (create-group (name (string-ascii 50)) (creator-nickname (string-ascii 32)))
  (let
    (
      (group-id (var-get next-group-id))
      (fee (var-get creation-fee))
      (user-group-list (default-to (list) (map-get? user-groups tx-sender)))
      (user-group-count (len user-group-list))
    )
    ;; Validations
    (asserts! (> (len name) u0) ERR-INVALID-NAME)
    (asserts! (<= (len name) u50) ERR-INVALID-NAME)
    (asserts! (> (len creator-nickname) u0) ERR-INVALID-NICKNAME)
    (asserts! (<= (len creator-nickname) u32) ERR-INVALID-NICKNAME)
    (asserts! (< user-group-count (var-get max-groups-per-user)) ERR-MAX-GROUPS-REACHED)

    ;; Handle fee payment if required
    (if (> fee u0)
      (begin
        (try! (stx-transfer? fee tx-sender CONTRACT-OWNER))
        (var-set total-fees-collected (+ (var-get total-fees-collected) fee))
      )
      true
    )

    ;; Create group info with stacks-block-time
    (map-set group-info group-id {
      name: name,
      creator: tx-sender,
      created-at: stacks-block-time, ;; Clarity 4: Unix timestamp
      active: true,
      treasury-contract: none
    })

    ;; Add to user's groups
    (map-set user-groups tx-sender
      (unwrap! (as-max-len?
        (append user-group-list group-id)
        u100) ERR-MAX-GROUPS-REACHED))

    ;; Add to all groups index
    (map-set all-groups-index (var-get all-groups-count) group-id)
    (var-set all-groups-count (+ (var-get all-groups-count) u1))

    ;; Increment group ID
    (var-set next-group-id (+ group-id u1))

    ;; AUTOMATICALLY initialize group in treasury contract
    (try! (contract-call? .group-treasury initialize-group group-id name tx-sender creator-nickname))

    ;; Emit event with native print (Clarity 4)
    (print {
      event: "group-created",
      group-id: group-id,
      creator: tx-sender,
      name: name,
      nickname: creator-nickname,
      timestamp: stacks-block-time ;; Clarity 4: Unix timestamp
    })
    (ok group-id)
  )
)

;; Add user to group (called by GroupTreasury contract when member is added)
(define-public (add-user-to-group (user principal) (group-id uint))
  (let
    (
      (group (unwrap! (map-get? group-info group-id) ERR-GROUP-NOT-FOUND))
      (user-group-list (default-to (list) (map-get? user-groups user)))
    )
    ;; Only the treasury contract or creator can call this
    (asserts! (get active group) ERR-GROUP-INACTIVE)

    ;; Check if user already has this group
    (if (is-some (index-of user-group-list group-id))
      (ok true) ;; Already a member, return success
      (begin
        ;; Add group to user's list
        (asserts! (< (len user-group-list) (var-get max-groups-per-user)) ERR-MAX-GROUPS-REACHED)
        (map-set user-groups user
          (unwrap! (as-max-len? (append user-group-list group-id) u100) ERR-MAX-GROUPS-REACHED))
        (print {
          event: "user-added-to-group",
          user: user,
          group-id: group-id,
          timestamp: stacks-block-time ;; Clarity 4
        })
        (ok true)
      )
    )
  )
)

;; Deactivate a group (only creator or contract owner)
(define-public (deactivate-group (group-id uint))
  (let
    (
      (group (unwrap! (map-get? group-info group-id) ERR-GROUP-NOT-FOUND))
    )
    (asserts!
      (or
        (is-eq tx-sender (get creator group))
        (is-eq tx-sender CONTRACT-OWNER)
      )
      ERR-UNAUTHORIZED
    )

    ;; Mark as inactive
    (map-set group-info group-id (merge group {active: false}))
    (print {
      event: "group-deactivated",
      group-id: group-id,
      timestamp: stacks-block-time ;; Clarity 4
    })
    (ok true)
  )
)

;; Set treasury contract address for a group (one-time setup)
(define-public (set-treasury-contract (group-id uint) (treasury principal))
  (let
    (
      (group (unwrap! (map-get? group-info group-id) ERR-GROUP-NOT-FOUND))
    )
    ;; Only creator can set treasury contract
    (asserts! (is-eq tx-sender (get creator group)) ERR-UNAUTHORIZED)
    (asserts! (is-none (get treasury-contract group)) ERR-UNAUTHORIZED) ;; Can only set once

    (map-set group-info group-id (merge group {treasury-contract: (some treasury)}))
    (print {
      event: "treasury-set",
      group-id: group-id,
      treasury: treasury,
      timestamp: stacks-block-time ;; Clarity 4
    })
    (ok true)
  )
)

;; Read-only functions

;; Get group information
(define-read-only (get-group-info (group-id uint))
  (map-get? group-info group-id)
)

;; Get user's groups
(define-read-only (get-user-groups (user principal))
  (default-to (list) (map-get? user-groups user))
)

;; Get active user groups only
(define-read-only (get-user-active-groups (user principal))
  (let
    (
      (user-group-list (default-to (list) (map-get? user-groups user)))
    )
    (filter is-group-active user-group-list)
  )
)

;; Helper to check if group is active
(define-read-only (is-group-active (group-id uint))
  (match (map-get? group-info group-id)
    group (get active group)
    false
  )
)

;; Get all active groups (paginated for efficiency)
(define-read-only (get-all-groups (offset uint) (limit uint))
  (let
    (
      (total-count (var-get all-groups-count))
      (end (if (< (+ offset limit) total-count) (+ offset limit) total-count))
    )
    {
      groups: (get-groups-range offset end),
      total: total-count
    }
  )
)

;; Helper to get groups in a range
(define-read-only (get-groups-range (start uint) (end uint))
  (map get-group-at-index (generate-sequence start end))
)

;; Get group ID at specific index
(define-read-only (get-group-at-index (index uint))
  (default-to u0 (map-get? all-groups-index index))
)

;; Generate sequence of numbers (helper for pagination)
(define-read-only (generate-sequence (start uint) (end uint))
  (if (<= start end)
    (list start)
    (list)
  )
)

;; Get total groups count
(define-read-only (get-total-groups-count)
  (var-get all-groups-count)
)

;; Get creation fee
(define-read-only (get-creation-fee)
  (var-get creation-fee)
)

;; Get max groups per user
(define-read-only (get-max-groups-per-user)
  (var-get max-groups-per-user)
)

;; Get next group ID (useful for frontend)
(define-read-only (get-next-group-id)
  (var-get next-group-id)
)

;; Admin functions (owner only)

;; Set creation fee
(define-public (set-creation-fee (new-fee uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-OWNER-ONLY)
    (var-set creation-fee new-fee)
    (print {
      event: "creation-fee-updated",
      new-fee: new-fee,
      timestamp: stacks-block-time ;; Clarity 4
    })
    (ok true)
  )
)

;; Set max groups per user
(define-public (set-max-groups-per-user (new-max uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-OWNER-ONLY)
    (asserts! (> new-max u0) ERR-INVALID-NAME)
    (asserts! (<= new-max u100) ERR-INVALID-NAME)
    (var-set max-groups-per-user new-max)
    (print {
      event: "max-groups-updated",
      new-max: new-max,
      timestamp: stacks-block-time ;; Clarity 4
    })
    (ok true)
  )
)

;; Withdraw collected fees (owner only)
(define-public (withdraw-fees)
  (let
    (
      (fees-amount (var-get total-fees-collected))
    )
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-OWNER-ONLY)
    (asserts! (> fees-amount u0) ERR-WITHDRAWAL-FAILED)

    (var-set total-fees-collected u0)
    (print {
      event: "fees-withdrawn",
      amount: fees-amount,
      timestamp: stacks-block-time ;; Clarity 4
    })
    (ok fees-amount)
  )
)

;; Get total fees collected
(define-read-only (get-total-fees-collected)
  (var-get total-fees-collected)
)

Functions (20)

FunctionAccessArgs
create-grouppublicname: (string-ascii 50
add-user-to-grouppublicuser: principal, group-id: uint
deactivate-grouppublicgroup-id: uint
set-treasury-contractpublicgroup-id: uint, treasury: principal
get-group-inforead-onlygroup-id: uint
get-user-groupsread-onlyuser: principal
get-user-active-groupsread-onlyuser: principal
is-group-activeread-onlygroup-id: uint
get-all-groupsread-onlyoffset: uint, limit: uint
get-groups-rangeread-onlystart: uint, end: uint
get-group-at-indexread-onlyindex: uint
generate-sequenceread-onlystart: uint, end: uint
get-total-groups-countread-only
get-creation-feeread-only
get-max-groups-per-userread-only
get-next-group-idread-only
set-creation-feepublicnew-fee: uint
set-max-groups-per-userpublicnew-max: uint
withdraw-feespublic
get-total-fees-collectedread-only