# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: estates.service-charge-reminders
title: Service charge reminders
sector: estates
source:
  kind: synthesized
  notes: >-
    No estate, facility manager or residents' association has built this with us yet. Written
    from the real-estate rent reminders (a due-date trait, a daily schedule over "due soon",
    "due today" and "overdue" segments, a call task when late) with a grace period before the
    first overdue message, a facility manager who calls, and strictly neutral wording. The
    sheet columns follow how estates usually keep their levy register: one row per unit.
derived_from: real-estate.rent-and-lease-reminders
vocabulary:
  person: resident
  people: residents
  group: estate executive
summary: >-
  Reminds residents before their service charge is due, on the day and when it is overdue, in
  plain factual words. If it is still unpaid after a grace period, your facility manager gets
  a task to call. The estate executive gets a monthly count of residents who are paid, due and
  overdue. Works for monthly, quarterly and yearly service charges.
when_to_use:
  - You run a residential estate, a gated community, a block of flats or a residents' association, and residents pay a service charge or levy.
  - Your estate office keeps a register in Google Sheets or Excel with each unit's due date and whether it has paid.
  - You chase payments by phone and WhatsApp one at a time, and the same few people end up doing it.
  - The executive wants a monthly picture of who has paid, without publishing anyone's name.

requires:
  data_in:
    - how: sheet_sync
      what: "Your residents register, synced every hour as People: phone, email, first name, unit, the service charge due date and its status (paid or due)."
    - how: csv_import
      what: "Or a one-off CSV of the same columns to start with."
  channels:
    - channel: sms
      for: The reminders and the overdue message (or WhatsApp, if you switch it on)
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
    - channel: whatsapp
      for: The same messages, if you answer yes to WhatsApp
      needs: A connected WhatsApp Business number, and each of the three templates approved by Meta.
    - channel: email
      for: The monthly report to the executive
      needs: Nothing extra. Reports go to your team members by email.
  events: []
  traits:
    - "{{ params.due_date_trait }}"
    - "{{ params.status_trait }}"
    - unit
    - charge_call
    - charge_note

parameters:
  - key: org_name
    ask: true
    question: What should messages call your estate or residents' association?
    label: Estate name
    type: string
    example: Northwind Gardens Estate
  - key: pay_info
    ask: true
    question: How do residents pay? (a few words, without account numbers)
    help: Say it the way you would say it out loud. Messages never carry bank details or amounts.
    label: How to pay
    type: text
    example: by bank transfer to the estate account, quoting your unit number
  - key: manager_team
    ask: true
    question: Which team calls residents whose service charge is overdue? (the facility manager, one person is fine)
    label: Facility manager
    type: team_name
    default: Facility manager
  - key: executive_team
    ask: true
    question: Who gets the monthly report? (for example the estate executive)
    label: Estate executive
    type: team_name
    default: Estate executive
  - key: use_whatsapp
    ask: true
    question: Should the reminders go by WhatsApp instead of SMS?
    help: Needs a connected WhatsApp Business number and Meta-approved templates.
    label: Use WhatsApp
    type: boolean
    default: false
  - key: office_phone
    label: Number residents can call with questions
    type: string
    default: "{{ workspace.phone }}"
  - key: due_date_trait
    label: Trait with the service charge due date
    type: trait_key
    default: service_charge_due_date
  - key: status_trait
    label: Trait that says if the current charge is paid (paid or due)
    type: trait_key
    default: service_charge_status
  - key: remind_before
    label: First reminder before the due date
    type: duration
    default: 14d
  - key: grace_period
    label: Grace period after the due date before the first overdue message
    type: duration
    default: 7d
  - key: overdue_window
    label: How long after the due date to keep following up
    type: duration
    default: 60d
  - key: call_due_hours
    label: Hours the facility manager has to make the call
    type: hours
    default: 72
  - key: send_time
    label: Daily send time
    type: time
    default: "09:00"
  - key: report_cron
    label: Monthly report (first day of the month)
    type: cron
    default: "0 8 1 * *"
  - key: timezone
    label: Time zone for the daily run and the report
    type: timezone
    default: "{{ workspace.timezone }}"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: resident

timings:
  - Runs every day at {{ params.send_time }} in {{ params.timezone }}.
  - A reminder when the service charge is due within {{ params.remind_before }}, another on the due day, and a factual message once it is more than {{ params.grace_period }} overdue, for up to {{ params.overdue_window }}. Residents marked paid get none of them.
  - "Two days after the overdue message, if the charge is still unpaid, {{ params.manager_team }} gets a call task due in {{ params.call_due_hours }} hours. A resident is chased at most once every three weeks."
  - "Monthly report to {{ params.executive_team }}: cron {{ params.report_cron }} in {{ params.timezone }}."

customer_must:
  - "Keep these columns in your residents register: phone, email, first_name, unit, {{ params.due_date_trait }} (a date like 2026-12-01) and {{ params.status_trait }} (exactly paid or due, never blank). Set the status to paid when the charge is received. When the next period starts, move the due date forward and set the status back to due."
  - Add your facility manager to the {{ params.manager_team }} team and your executive to {{ params.executive_team }} (Settings → Collaborators → Teams). A task or report sent to an empty team reaches nobody.
  - "If you chose WhatsApp: connect your WhatsApp Business number and submit the three templates (service_charge_due_soon, service_charge_due_today and service_charge_overdue) for Meta approval before switching the follow-ups on."
  - Connect an SMS provider or finish business verification. Tell residents at a meeting or in the estate group that reminders will come by SMS or WhatsApp and that they can reply STOP.
  - Keep every message factual. Never link payment to gate access, utilities, visitors or any other service, and never name or list residents who haven't paid. Have the executive agree this wording at a meeting before you switch it on.
  - Check the segment sizes against your register, then switch the follow-ups and the report on and test with your own phone and a fake unit.

objects:
  - ref: team_manager
    kind: team
    about: Calls residents whose service charge is still unpaid after the grace period.
    body: { name: "{{ params.manager_team }}" }
  - ref: team_executive
    kind: team
    about: Gets the monthly report.
    body: { name: "{{ params.executive_team }}" }

  - ref: seg_due_soon
    kind: segment
    about: Service charge due within the reminder window and not paid.
    body:
      name: Service charge due soon
      description: "{{ params.due_date_trait }} within the next {{ params.remind_before }}, not paid."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.due_date_trait }}", cond_operator: within_next, value: "{{ params.remind_before }}" }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: paid }
  - ref: seg_due_today
    kind: segment
    about: Service charge due today (the date started in the last day) and not paid.
    body:
      name: Service charge due today
      description: "{{ params.due_date_trait }} is today, not paid."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.due_date_trait }}", cond_operator: within_last, value: 1d }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: paid }
  - ref: seg_overdue
    kind: segment
    about: Overdue by more than the grace period, within the follow-up window, and not paid.
    body:
      name: Service charge overdue
      description: "{{ params.due_date_trait }} more than {{ params.grace_period }} ago but within {{ params.overdue_window }}, not paid."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.due_date_trait }}", cond_operator: older_than, value: "{{ params.grace_period }}" }
          - { type: trait, trait_key: "{{ params.due_date_trait }}", cond_operator: within_last, value: "{{ params.overdue_window }}" }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: paid }
  - ref: seg_paid
    kind: segment
    about: Residents marked as paid.
    body:
      name: Service charge paid
      description: "{{ params.status_trait }} is paid."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: eq, value: paid }

  - ref: tmpl_soon_sms
    kind: template
    about: SMS before the service charge is due.
    body:
      name: Service charge due soon (SMS)
      channel: sms
      category: Service charge
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, a reminder from {{ params.org_name }}: the service charge for {{ traits.unit | default: \"your unit\" }} is due on {{ traits.{{ params.due_date_trait }} | date: \"%-d %b %Y\" }}. Pay {{ params.pay_info }}. Already paid? Thank you. Questions? Call {{ params.office_phone }}. Reply STOP to opt out."
  - ref: tmpl_today_sms
    kind: template
    about: SMS on the due day.
    body:
      name: Service charge due today (SMS)
      channel: sms
      category: Service charge
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, {{ params.org_name }} here: the service charge for {{ traits.unit | default: \"your unit\" }} is due today. Pay {{ params.pay_info }}. Already paid? Thank you. Questions? Call {{ params.office_phone }}. Reply STOP to opt out."
  - ref: tmpl_overdue_sms
    kind: template
    about: SMS once the charge is overdue. Factual, with an offer to talk.
    body:
      name: Service charge overdue (SMS)
      channel: sms
      category: Service charge
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, {{ params.org_name }} here. We have not yet recorded the service charge for {{ traits.unit | default: \"your unit\" }} that was due on {{ traits.{{ params.due_date_trait }} | date: \"%-d %b %Y\" }}. Pay {{ params.pay_info }}. If something is in the way, please call {{ params.office_phone }} so we can talk. Reply STOP to opt out."
  - ref: tmpl_soon_wa
    kind: template
    when: { param: use_whatsapp, equals: true }
    about: WhatsApp reminder before the charge is due (needs Meta approval).
    body:
      name: Service charge due soon (WhatsApp)
      channel: whatsapp
      category: Service charge
      style: text
      status: active
      subject: ""
      provider_template_id: service_charge_due_soon
      metadata: { whatsapp: { category: UTILITY, language: en } }
      variables:
        "1": "{{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
        "2": "{{ params.org_name }}"
        "3": "{{ traits.unit | default: \"your unit\" }}"
        "4": "{{ traits.{{ params.due_date_trait }} | date: \"%-d %b %Y\" }}"
        "5": "{{ params.pay_info }}"
        "6": "{{ params.office_phone }}"
      body: "Hello {{1}}, a reminder from {{2}}: the service charge for {{3}} is due on {{4}}. Pay {{5}}. If you have already paid, thank you. Questions? Call {{6}} any time."
  - ref: tmpl_today_wa
    kind: template
    when: { param: use_whatsapp, equals: true }
    about: WhatsApp reminder on the due day (needs Meta approval).
    body:
      name: Service charge due today (WhatsApp)
      channel: whatsapp
      category: Service charge
      style: text
      status: active
      subject: ""
      provider_template_id: service_charge_due_today
      metadata: { whatsapp: { category: UTILITY, language: en } }
      variables:
        "1": "{{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
        "2": "{{ params.org_name }}"
        "3": "{{ traits.unit | default: \"your unit\" }}"
        "4": "{{ params.pay_info }}"
        "5": "{{ params.office_phone }}"
      body: "Hello {{1}}, this is {{2}}: the service charge for {{3}} is due today. Pay {{4}}. If you have already paid, thank you. Questions? Call {{5}} any time."
  - ref: tmpl_overdue_wa
    kind: template
    when: { param: use_whatsapp, equals: true }
    about: WhatsApp message once the charge is overdue (needs Meta approval).
    body:
      name: Service charge overdue (WhatsApp)
      channel: whatsapp
      category: Service charge
      style: text
      status: active
      subject: ""
      provider_template_id: service_charge_overdue
      metadata: { whatsapp: { category: UTILITY, language: en } }
      variables:
        "1": "{{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
        "2": "{{ params.org_name }}"
        "3": "{{ traits.unit | default: \"your unit\" }}"
        "4": "{{ traits.{{ params.due_date_trait }} | date: \"%-d %b %Y\" }}"
        "5": "{{ params.pay_info }}"
        "6": "{{ params.office_phone }}"
      body: "Hello {{1}}, this is {{2}}. We have not yet recorded the service charge for {{3}} that was due on {{4}}. Pay {{5}}. If something is in the way, please call {{6}} so we can talk about it."

  - ref: report_monthly
    kind: report
    about: Each month, how many residents are due, paid and overdue, to the estate executive.
    body:
      name: "Monthly: service charge"
      description: How many residents are due soon, have paid and are overdue, with the state of the facility manager's calls. Counts only, no names.
      schedule_cron: "{{ params.report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: previous_month
        compare: previous_period
        week_start: monday
        sections:
          - id: sizes
            kind: segment_sizes
            title: Service charge now
            params: { segment_ids: ["{{ ref.seg_due_soon.id }}", "{{ ref.seg_paid.id }}", "{{ ref.seg_overdue.id }}"] }
          - { id: tasks, kind: tasks_summary, title: Facility manager calls, params: {} }
          - { id: note, kind: text, params: { body: "These are the numbers as the register stands today. Residents marked paid stay counted until the next period starts." } }
      recipients: [{ type: team, id: "{{ ref.team_executive.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_soon
    kind: workflow
    about: 'Daily run over "service charge due soon": one reminder per period.'
    body:
      name: "Service charge 1: due soon"
      description: Every day, a reminder to residents whose service charge is due soon.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_due_soon.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, when: { param: use_whatsapp, equals: false }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_soon_sms.id }}" } } } }
        - { id: s2, type: action, when: { param: use_whatsapp, equals: true }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: whatsapp, template_id: "{{ ref.tmpl_soon_wa.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 480
      goal: {}
      exit_conditions: []
  - ref: wf_today
    kind: workflow
    about: 'Daily run over "service charge due today": one reminder.'
    body:
      name: "Service charge 2: due today"
      description: Every day, a reminder to residents whose service charge is due today.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_due_today.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, when: { param: use_whatsapp, equals: false }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_today_sms.id }}" } } } }
        - { id: s2, type: action, when: { param: use_whatsapp, equals: true }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: whatsapp, template_id: "{{ ref.tmpl_today_wa.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 72
      goal: {}
      exit_conditions: []
  - ref: wf_overdue
    kind: workflow
    about: 'Daily run over "service charge overdue": a factual message, then a call task for the facility manager if still unpaid.'
    body:
      name: "Service charge 3: overdue"
      description: A factual message to residents whose service charge is overdue, then a call task for the facility manager if it is still unpaid. The outcome is saved on the resident.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_overdue.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, when: { param: use_whatsapp, equals: false }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_overdue_sms.id }}" } } } }
        - { id: s2, type: action, when: { param: use_whatsapp, equals: true }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: whatsapp, template_id: "{{ ref.tmpl_overdue_wa.id }}" } } } }
        - { id: s3, type: delay, config: { duration: 2d } }
        - { id: s4, type: filter, config: { condition: { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: paid } } }
        - id: s5
          type: human_task
          config:
            human_task:
              title: "Service charge: {{ traits.first_name }} {{ traits.last_name }}, {{ traits.unit | default: \"unit not given\" }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) has not paid the service charge that was due, even after a message.

                Check the register first: if they have paid, update the sheet and close this task. Otherwise call the resident only, politely and factually. Ask if something is in the way and offer to bring a payment arrangement to the executive. Do not mention gate access, utilities, visitors or other residents. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_manager.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: will_pay, label: Promised a date, intent: positive }
                - { id: paid, label: Already paid, intent: positive }
                - { id: arrangement, label: Needs a payment arrangement, intent: neutral }
                - { id: query, label: Queries the charge, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.call_due_hours }}"
              wait: true
              wait_timeout_hours: 240
              notify: immediate
        - { id: s6, type: action, config: { action_type: set_trait, traits: { charge_call: "{{ properties.task_outcome | default: \"unknown\" }}", charge_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 504
      goal: {}
      exit_conditions: []

eval:
  name: 113_estates_service_charge_reminders
  mode: task
  vertical: ""
  prompt: >-
    I manage a residential estate. Our residents sheet has phone, unit, service_charge_due_date
    and service_charge_status (paid or due). Text residents two weeks before the service charge
    is due, on the day, and again if it is a week overdue, and give our facility manager a task
    to call anyone still unpaid. Send the estate executive a monthly report of how many are
    paid, due and overdue. Keep the messages polite and factual, with no amounts.
  expect_artifacts: [segment, workflow, message_template, team, report]
  forbid_terms: [disconnect, cut off, gate access, evict]
