SIEM server · Source-backed workflow catalogue

What each BullMQ job does

One card per job type, with its own flow diagram: queued by → checks and branches → work → writes, emails and next jobs. Billing-cycle has three linked diagrams so the charge logic and saved-card changes remain readable.

29 live job types5 unused / placeholder / retired paths36 workflow pages36/36 automated checks passed

Read a card first. It explains the trigger and steps; open the full flow for branches, or expand exact behavior for record names, email subjects, retries and source evidence.

Job completion is not always business success. Some sweeps catch per-row errors; system-alerts catches its outer error. Cards state where a failure is logged instead of retried.

Snapshot: 14 Oct 2026 · server commit 423f04fc5cd5. Cron times have no explicit timezone in these schedules. Source inspection confirms registration and code paths, not jobs currently stored in Redis.

34 cards shown

Charges, prices and settlement

7 job types
Live job typeArtifact + browser checks passed

task:billing-cycle

Takes a first charge or renewal after status, cycle, cancellation, downgrade, pricing and mandate checks. Also handles a paid change after a Stripe card is saved.

Triggered by: Activation, renewal — card saved, reconciliation

  1. Job firesbilling-cycle-<sub>-<n>
  2. Status + cycle valid?0: UNPAID/PAYMENT_FAILED
  3. Period ended?ACTIVE, cycle ≥ 1 only
  4. Cancel flag set?cancelAtPeriodEnd
  5. Downgrade scheduled?downgradePlanId set
  6. Charge stepcontinues in flow 1b
  7. Locked price set?lockedPriceAmount
  8. Amount ≤ 0?price − running discount
  9. Card on file?provider + externalTokenId
  10. Razorpay ceiling ok?only when price pushed
  11. Lock + chargecharge-in-flight, 24 h
  12. Gateway resultStripe or Razorpay
  13. Job fires now…-<cycle>-<reason>
  14. Change still valid?status + same cycle
  15. Stripe card saved?externalTokenId
  16. Amount ≤ 0?discount or tiny proration
  17. Chargelock unless upgrade
  18. Stripe resultwebhook decides

Stops / waits: Skip: stale job (≥1 needs ACTIVE/PAST_DUE); Re-delay job (until currentPeriodEnd); Settled internally (no gateway call); PAST_DUE + grace (no-payment-method email); PENDING_REAUTH (grace + email, no charge); Skip: lock held (other charge settles it); Unknown (Stripe) (stripe-charge-reconcile); Skip (status or cycle moved); Skip: no card (nothing charged); Declined (email, nothing changes).

Exact behavior, writes, messages and failure handling

task:billing-cycle — checks before charging

  • Queued by: admin activation (cycle 1, delayed to the first period end); settlement follow-up after each paid renewal (next cycle at the new period end); Stripe TOKEN_SAVED (delay 0: the cycle-0 first charge, or a PAST_DUE recovery charge); an upgrade that changes the interval (job replaced at the new period end); task:billing-cycle-reconciliation (lost job). Job ID billing-cycle-<sub>-<cycle>, attempts 1.
  • Status + cycle check: cycle 0 runs only for UNPAID or PAYMENT_FAILED; cycle ≥ 1 only for ACTIVE or PAST_DUE. The job's cycle must equal subscriptions.billingCycleNumber, otherwise it is a stale job and does nothing.
  • Period check: an ACTIVE subscription (cycle ≥ 1) whose currentPeriodEnd is still ahead is moved back to delayed until that time. PAST_DUE and cycle 0 run at once.
  • Cancel flag: beginCancellationGracePeriod → status CANCELLED, expiryDate = now + grace (7 / 15 / 30 days by interval), grace-warn-3d, grace-warn-1d and grace-final queued, email 'Your Paid Access Has Ended'. No charge. The job ends here.
  • Scheduled downgrade: planId, assetsReqCount, interval, intervalDays and lockedPriceAmount switch to the downgrade; discountAmountPerCycle recomputed (percentage follows the new price, fixed stays); pushed price cleared; downgrade fields cleared; agents marked pendingRemoval are deleted (asset_logs) and admins get 'Action Required: Remove Agents from the SIEM Server (Downgrade)'. Then the charge step runs with the new price.

task:billing-cycle — charge and outcome

  • Locked price missing → status PAST_DUE and startPaymentGracePeriod (expiryDate, grace-warn-3d/1d, grace-final). A missing price is never treated as a free cycle.
  • Amount = cycle price (pendingPriceAmount once its cycle is reached and the customer was emailed, else lockedPriceAmount) − running discount (while discountCyclesUsed < discountCyclesTotal; null = forever). ≤ 0 → PaymentEventProcessor gets an internal PAYMENT_SUCCEEDED (externalId internal_zero_<sub>_cycle_<n>, 'Fully discounted — no payment taken'); period advances like a paid renewal.
  • No provider or card → PAST_DUE + grace + email 'Payment Failed - Action Required': no payment method on file, amount due, days left to add one.
  • Razorpay ceiling: only when a pushed price applies. Price above mandateMaxAmount → PENDING_REAUTH, paymentFailureSubReason PRICE_EXCEEDS_MANDATE, grace started, email asking to re-authorize. The old price is not charged.
  • Lock + charge: Redis key charge-in-flight:<sub> (NX, 24 h). Held by another charge → job ends; that charge settles the cycle. Cycle 0 also claims the single signup debit (signupPaymentInFlight). chargeOffSession with idempotency key sub_<id>_cycle_<n>_<token> and metadata internalSubscriptionId, billingCycleNumber, retryAttempt 0, discountAmountApplied.
  • Declined at once → PAYMENT_FAILED event processed now: cycle 0 → PAYMENT_FAILED (Stripe checkout closed, coupon slot released, one email); cycle ≥ 1 → PAST_DUE, first failure emails and starts grace, retry-payment +3 days if the reason is temporary. Job marked failed in Bull Board.
  • Success or pending → nothing written here; the gateway webhook settles it (renewal: ACTIVE, period from old end + interval, cycle + 1, invoice, settlement follow-up). Stripe unknown outcome → lock kept, stripe-charge-reconcile queued.

task:billing-cycle (plan change paid with a just-saved Stripe card)

  • Queued by: Stripe setup_intent.succeeded (TOKEN_SAVED) whose setup carried a plan change — trial conversion, an upgrade with no usable card, a reactivation with no card; and by upgradeSubscription directly for a Stripe reactivation with a card on file. Job ID billing-cycle-<sub>-<cycle>-<reason> (trial_upgrade, upgrade, reactivation) so the cycle's delayed renewal job does not swallow it. Delay 0.
  • Still valid: trial_upgrade needs the current plan still free and status TRIALING or ENDED; upgrade needs ACTIVE; reactivation needs CANCELLED; the subscription's cycle must equal the job's.
  • Amount ≤ 0 (a running discount covers it, or an upgrade proration under 0.50) → internal PAYMENT_SUCCEEDED with the change attached; the change is applied without a gateway call.
  • Charge: trial conversion and reactivation take the charge lock; an upgrade proration does not. Idempotency key sub_<id>_<reason>_<cycle>_<plan>_<assets>_<interval>_<token>. Unknown outcome → stripe-charge-reconcile.
  • Paid (webhook): trial → ACTIVE, plan applied, period starts now, paid cycle 1, next renewal cycle 2, trial and grace jobs removed, 'Payment Successful' + PDF, admins 'Payment Received'. Reactivation → ACTIVE, chosen plan, new period from payment, cycle + 1, 'Payment Successful'. Upgrade → plan, assets, price applied; changed interval restarts the period and replaces the renewal job; 'Subscription Upgrade Details'. Agents not kept are deleted and admins emailed.
  • Declined: lock released (trial/reactivation), email 'Payment Failed - Action Required' saying the change did not happen; status, plan and period unchanged; no grace, no retry.
Live job typeArtifact + browser checks passed

task:retry-payment

Retries a temporarily declined renewal, while preserving cycle checks and the single-charge lock.

Triggered by: Renewal declined — temporary reason only

  1. Job fires +3 daysattempt 1, 2 or 3
  2. Still PAST_DUE?same cycle, card, price
  3. Amount ≤ 0?price − discount
  4. Charge lock free?charge-in-flight:<sub>
  5. Charge againkey …_retry_<k>
  6. Gateway resultStripe or Razorpay

Stops / waits: Skip (paid, expired, no card); Wait 15 minutes (same attempt runs again); Declined again (next attempt if temporary).

Exact behavior, writes, messages and failure handling

task:retry-payment

  • Queued by: PaymentEventProcessor.handlePaymentFailed for a renewal (cycle ≥ 1) when the failure reason is temporary — Stripe insufficient_funds, processing_error, issuer_not_available, try_again_later and similar; Razorpay insufficient_funds, bank/gateway/issuer technical errors, payment_timed_out and similar — and it is not a Razorpay mandate-limit breach. Failed attempt 0 (the renewal) → attempt 1, 1 → 2, 2 → 3. A manual 'Retry Now' failure queues nothing.
  • Job ID retry-payment-<sub>-cycle<n>-attempt<k>, delay 3 days, attempts 1. Completed jobs are kept so a redelivered failure webhook cannot add the same attempt twice.
  • Still PAST_DUE: status must be PAST_DUE, cycle unchanged, provider, card and locked price present. Paid in the meantime, expired, or token revoked → nothing happens.
  • Amount = pushed or locked price − running discount, the same as the original renewal. ≤ 0 (a discount added since) → internal settlement, chain ends.
  • Lock held (a manual retry in flight) → job moved back to delayed for 15 minutes and runs again; if the other charge settles the cycle, the PAST_DUE check then skips it.
  • Paid → webhook: ACTIVE, new period starts at the payment date (late payment), cycle + 1, grace and retry jobs removed, 'Payment Successful' + PDF. Declined → PAYMENT_FAILED event: no email, no new grace; the next attempt only for a temporary reason, at most 3. Monthly grace is 7 days, so attempt 3 (day 9) never runs for monthly plans.
Source evidence · pinned commitsrc/bullmq/workers/retryPaymentWorker.ts:24 ↗
Live job typeArtifact + browser checks passed

task:plan-price-push

Finds customers whose recurring price differs and queues an individual price update for each.

Triggered by: Super admin push — save or push button

  1. Job firesattempts 5
  2. List plan's subs4 billing statuses
  3. Price differs?plan rate vs current
  4. Gateway pair valid?country vs gateway
  5. Add one job eachaddBulk

Stops / waits: Same price (left alone); Logged unpushable (reason in job log).

Exact behavior, writes, messages and failure handling

task:plan-price-push

  • Queued by: a super admin saving changed plan prices and choosing 'New and existing customers' (PUT /plan/plans/:id with applyToExisting), or 'Push new price to these' in Plan Details (POST /plan/plans/:id/price-push). No fixed job ID; attempts 5, exponential backoff from 5 s.
  • List: subscriptions of this plan in ACTIVE, PAST_DUE, PENDING_REAUTH or PENDING_SETUP with a locked price and a provider. A free plan has nothing to push.
  • Price differs: the plan rate for the subscription's own currency, interval and asset count, compared with what it pays now (pendingPriceAmount if one is staged, else lockedPriceAmount). Equal → left alone.
  • Gateway pair: getSubscriptionBilling must accept the stored company country and gateway; a mismatch is written to the job log as not pushed.
  • Add one job each: one task:subscription-price-push per subscription in a single addBulk, job ID price-push-<plan>-<sub>-<price> (a repeat of the same push adds nothing). The preview endpoint uses the same comparison to count subscriptions and Razorpay re-authorizations before the admin confirms.
Source evidence · pinned commitsrc/services/pricePush.service.ts:57 ↗
Live job typeArtifact + browser checks passed

task:subscription-price-push

Stages one customer’s new recurring price, preserves their discount and sends advance notice.

Triggered by: Plan push fan-out — flow 4

  1. Job firesconcurrency 5, attempts 5
  2. Still applies?status, plan, assets
  3. Stage new pricenot charged yet
  4. Email customerPrice Update Notification
  5. Record email time24 h notice re-checked
  6. Charged at renewalfrom pendingPriceFromCycle

Stops / waits: Skip (changed or already told); Send failed (job throws, retried).

Exact behavior, writes, messages and failure handling

task:subscription-price-push

  • Queued by plan-price-push. Job ID price-push-<plan>-<sub>-<price>; concurrency 5; attempts 5 with backoff.
  • Still applies: status ACTIVE, PAST_DUE, PENDING_REAUTH or PENDING_SETUP; same plan, asset count and interval the push was priced for (a plan change since then priced itself); not already staged and notified at this price.
  • Stage: pendingPriceAmount = new price; pendingPriceFromCycle = the next renewal if it is more than 24 hours away, otherwise the renewal after it (PENDING_SETUP → cycle 1, the first renewal after activation); pendingDiscountAmountPerCycle = running discount on the new price (percentage recomputed, fixed kept); priceUpdateNotifiedAt cleared.
  • Email 'Subscription Price Update Notification': old and new price per cycle, the renewal date it is first charged, 'Your running discount continues…' when one runs, and for Razorpay above the mandate 'You will be asked to re-authorize it before this renewal'. A failed send throws; the job is retried.
  • Record: priceUpdateNotifiedAt = time of the email; pendingPriceFromCycle moved one renewal later if the email went out less than 24 hours before the staged renewal. Until this is set the new price is never charged.
  • At renewal: the cycle is charged at the new price less the running discount; on success lockedPriceAmount and discountAmountPerCycle take the new values and priceUpdateAppliedAt is set. Razorpay with the new price above mandateMaxAmount → PENDING_REAUTH, no charge, re-authorization needed. Any plan change clears the pushed price.
Source evidence · pinned commitsrc/services/pricePush.service.ts:109 ↗
Live job typeArtifact + browser checks passed

task:stripe-charge-reconcile

Finds the outcome of a Stripe charge that may already have succeeded without creating a second charge.

Triggered by: Unknown Stripe result — 2 / 4 / 8 … 60 min

  1. Read original requestkey, metadata, first time
  2. Extend charge lockexcept upgrade proration
  3. Inside 24 hours?idempotency window
  4. Repeat identical chargesame idempotency key
  5. Record known outcomePaymentEventProcessor

Stops / waits: Unknown → next check (attempt +1, same first time).

Exact behavior, writes, messages and failure handling

task:stripe-charge-reconcile

  • An unknown Stripe charge queues the full original params, attempt and firstAttemptAt; delay min(2 × 2^attempt,60) minutes. ID stripe-reconcile-<key>-<attempt>; three execution attempts per queued check. stripeChargeReconcile.ts:25–31; repeatableQueue.ts:6–11.
  • Extend subscription charge lock except upgrade proration, which never held it. stripeChargeReconcile.ts:34–41.
  • Within 24 hours replay the same request/key. Unknown → queue another check. Succeeded/failed → normalized processor event. A normal pending result is left to its webhook. stripeChargeReconcile.ts:43–50,67–104.
  • After 24 hours do not replay: search existing PaymentIntents by charge metadata. Pending → check again; final result → process; absent → PAYMENT_FAILED with charge_not_created. stripeChargeReconcile.ts:53–64.
  • The processor owns invoice/status/discount settlement, grace and next jobs. This job preserves original reason, cycle and plan-change metadata.
Live job typeArtifact + browser checks passed

task:billing-cycle-reconciliation

Restores missing renewal jobs without racing existing jobs or in-flight debits.

Runs: Every hour, on the hour.

  1. Find paid ACTIVE rowsperiod end ≤ now +1 h
  2. Renewal job exists?waiting, delayed, active
  3. Overdue by 15 min?else future-only repair
  4. Check charge in flightRedis + Razorpay pending
  5. Restore billing jobsame sub + cycle ID

Stops / waits: Excluded subscriptions (free / no provider / other status); Keep existing job (already pending execution); No repair in margin (ended ≤15 min ago); Wait for charge result (lock / pending debit).

Exact behavior, writes, messages and failure handling

task:billing-cycle-reconciliation

  • Hourly 0 * * * *; no explicit cron timezone. repeatableQueue.ts:132–137; repeatableWorker.ts:1075–1079.
  • Select ACTIVE non-free subscriptions with provider and period end within one hour or earlier. Existing waiting/delayed/active renewal → skip. billingCycleQueue.ts:73–91.
  • Within overdue margin queue only a future renewal with no job at all; ended ≤15 minutes ago → leave alone. More than 15 minutes overdue → skip Redis charge lock or pending RazorpayChargeAttempt. billingCycleQueue.ts:93–105.
  • Restore task:billing-cycle with current cycle and period end; negative delay becomes zero. Completed/failed retained job is removed before replacement. The billing worker rechecks before charging. billingCycleQueue.ts:29–46,108–123.
  • No invoice, status write, charge or email occurs directly in this reconciliation job.
Live job typeArtifact + browser checks passed

task:payment-settlement-follow-up

Completes onboarding or renewal scheduling and invoice emails after a payment settlement commits.

Triggered by: Settlement committed — or maintenance recovery

  1. Load settled invoicenot already followed up
  2. Load owner and planmissing → throw
  3. First-charge invoice?billingCycleNumber = 0
  4. Run follow-up branchonboarding or renewal
  5. Mark follow-up finishedsettlementFollowUpAt

Stops / waits: Skip already done (or settlement not applied); Retry unfinished work (failed email throws).

Exact behavior, writes, messages and failure handling

task:payment-settlement-follow-up

  • After commit queue settlement-follow-up-<invoiceId>. Maintenance recovers up to 100 unfinished settled invoices and claims each for five minutes. PaymentEventProcessor.ts:734–755.
  • Skip unapplied/already-followed-up invoice. Missing subscription, owner or plan throws. PaymentEventProcessor.ts:758–765.
  • Cycle 0: create membership if absent, bootstrap owner IAM, cancel lifecycle jobs, rebuild reminders, send onboarding emails: admin pending setup, customer in-progress with PDF, onboarding instructions. PaymentEventProcessor.ts:775–787; subscription.service.ts:817–885.
  • Later invoice: only latest paid cycle on ACTIVE with period end queues next billing, cancels lifecycle/retry jobs and rebuilds reminders. Old recovered follow-up must not cancel newer grace. PaymentEventProcessor.ts:789–801.
  • Later payment email includes invoice PDF. Save delivered email keys immediately; failed send throws so retry skips saved emails. Invalidate cache, set settlementFollowUpAt, clear nextSettlementCheckAt. PaymentEventProcessor.ts:768–774,805–819.
  • Sending and recording an email are separate operations; the marks prevent ordinary retry duplicates, not a strict exactly-once delivery guarantee.

Trials, grace and temporary access

6 job types
Live job typeArtifact + browser checks passed

task:trial-ending

Ends a claim-based free trial and schedules its remaining seven-day grace period.

Triggered by: Free plan claimed — at trialEnd

  1. Job firestrial-end-<sub>
  2. Still a trial?PENDING_SETUP / TRIALING
  3. trialEnd passed?set and in the past
  4. Move to ENDEDno SIEM access
  5. Queue grace jobsremaining days
  6. Email customersent directly

Stops / waits: Skip (converted or ended); Skip (not yet / missing date).

Exact behavior, writes, messages and failure handling

task:trial-ending

  • Queued by: the free plan claim (createAndInitiatePayment, isFreePlan) at trialEnd = claim time + plan.freeTrialDays. Job ID trial-end-<sub>; attempts 3 with backoff.
  • Still a trial: isTrialSubscription true and status PENDING_SETUP (claimed, not yet activated by an admin) or TRIALING. A paid trial conversion removes this job and clears the trial flag.
  • trialEnd passed: the claim deadline, not the activation date, ends the trial. Missing trialEnd is logged as an error.
  • Move to ENDED: status ENDED, isTrialActive false, expiryDate = trialEnd + 7 days (FREE_TRIAL_GRACE_PERIOD_DAYS). Customer keeps team, profile, subscription and wallet view; no SIEM. Can still convert to a paid plan in place until expiry.
  • Grace jobs: grace-final-<sub> at expiryDate and grace-warn-3d / grace-warn-1d for the remaining days.
  • Email 'Your Free Trial Has Ended'. The 00:00 sub-finalization sweep performs the same steps for a trial whose job was lost (plus an in-app 'Trial Period Ended').
Source evidence · pinned commitsrc/bullmq/workers/lifecycleWorker.ts:26 ↗
Live job typeArtifact + browser checks passed

task:grace-warning

Warns a customer three days and one day before grace-period deactivation.

Triggered by: Grace period starts — 4 statuses, see card

  1. Job firesgrace −3 d and −1 d
  2. Still in grace?status unchanged
  3. expiryDate set?deactivation date
  4. Send warningsent directly

Stops / waits: Skip (paid, renewed, converted); Skip, log error (data integrity).

Exact behavior, writes, messages and failure handling

task:grace-warning

  • Queued together with final-deactivation every time a grace period starts: startPaymentGracePeriod (renewal declined → PAST_DUE; no card or no price at renewal → PAST_DUE; Razorpay pushed price above the mandate → PENDING_REAUTH), beginCancellationGracePeriod (period end with cancel flag → CANCELLED), and trial-ending / sub-finalization (→ ENDED).
  • Two jobs: grace-warn-3d-<sub> at grace length − 3 days and grace-warn-1d-<sub> at grace length − 1 day; a warning whose time is already past is not queued. Grace length: monthly 7, quarterly 15, yearly 30 days (STRIPE_*_GRACE_PERIOD_DAYS), free trial 7 days after trialEnd.
  • Still in grace: status must still be ENDED, CANCELLED, PAST_DUE or PENDING_REAUTH. A payment that settles also removes these jobs (cancelAllSubscriptionLifecycleJobs).
  • Email 'Deactivation Warning: Data Deletion in N Day(s)' to the owner, stating subscriptions.expiryDate as the deactivation date.
Source evidence · pinned commitsrc/bullmq/workers/lifecycleWorker.ts:75 ↗
Live job typeArtifact + browser checks passed

task:final-deactivation

Deletes local agent rows, expires the subscription and requests manual remote cleanup.

Triggered by: Grace period starts — same as warnings

  1. Job firesgrace-final-<sub>
  2. Still in grace?status unchanged
  3. Delete agentsportal + database only
  4. All deleted?every agent row
  5. Move to EXPIREDnot recoverable
  6. Switch workspaceowner's current sub

Stops / waits: Skip (paid or renewed); Job throws (retried, status kept).

Exact behavior, writes, messages and failure handling

task:final-deactivation

  • Queued at the same moments as grace-warning. Job ID grace-final-<sub>, delay = grace length (monthly 7, quarterly 15, yearly 30 days; trial: until trialEnd + 7 days). attempts 3 with backoff.
  • Still in grace: status must still be ENDED, CANCELLED, PAST_DUE or PENDING_REAUTH. Paying or renewing inside grace removes the job; if it runs anyway it does nothing.
  • Delete agents: every assets row of the subscription gets an asset_logs entry (action REMOVED, reason 'Trial Expiry' or 'Cancellation/Payment Failure') and is deleted. Admins get 'Action Required: Remove Agents from the SIEM Server: <company> (<reason>)' with the agent list. The SIEM/Wazuh server is not changed.
  • If any agent could not be deleted the job throws before the status changes; BullMQ retries it and the retry deletes the rest.
  • Move to EXPIRED. Customer: 'Service Deactivation: Data Permanently Removed'. Admins: 'Subscription Expired: <company>'. EXPIRED gives profile and subscription (buy new) only.
  • Switch workspace: ensureUserHasDefaultSubscription moves the owner's currentSubscriptionId to another owned non-EXPIRED subscription, else to an ACTIVE/TRIALING team membership, else null.
Source evidence · pinned commitsrc/bullmq/workers/lifecycleWorker.ts:136 ↗
Live job typeArtifact + browser checks passed

task:mandate-reauth-reminder

Asks a customer to authorize a higher Razorpay mandate after a limit-related renewal failure.

Triggered by: Mandate limit hit — Razorpay renewal failed

  1. Job firesnow, then mid-grace
  2. Still unpaid?PAST_DUE / PENDING_REAUTH
  3. Read cycle pricepushed or locked
  4. Send remindersent directly

Stops / waits: Skip (re-authorized or expired).

Exact behavior, writes, messages and failure handling

task:mandate-reauth-reminder

  • Queued by: PaymentEventProcessor.handlePaymentFailed on the failure that moves a renewal to PAST_DUE when Razorpay's error description says the amount is above the mandate's maximum (isMandateLimitBreach). The subscription gets paymentFailureSubReason MANDATE_LIMIT_EXCEEDED, the normal grace jobs, and no automatic retry (the same charge would fail the same way).
  • Not queued for PENDING_REAUTH created by a price push at renewal; that path only gets the grace warnings and the one 'Payment Failed - Action Required' email from billingCycleWorker.
  • Two jobs: mandate-reauth-0-<sub> immediately and mandate-reauth-mid-<sub> at half the grace period (monthly: day 3; quarterly: day 7; yearly: day 15).
  • Email 'Payment Failed - Action Required': the subscription amount is higher than the authorized limit, re-authorize the mandate with a higher limit, N day(s) remaining; amount = the cycle's full price; link /portal/subscription (the Re-authorize Mandate / payment setup box).
Source evidence · pinned commitsrc/bullmq/workers/lifecycleWorker.ts:102 ↗
Live job typeArtifact + browser checks passed

task:admin-membership-expiry

Revokes an admin’s temporary customer membership and clears access caches.

Triggered by: Admin joins workspace — for N hours

  1. Job firesafter N hours
  2. Remove membershipthis subscription
  3. Detach owner policyTenant-Owner-Default
  4. Delete boundariesall of this admin user
  5. Reset workspaceif it was this one
  6. Clear cachesIAM + auth context
Exact behavior, writes, messages and failure handling

task:admin-membership-expiry

  • Queued by: a super admin joining a customer's subscription as a hidden member for N hours (admin-membership controller; refused for EXPIRED or DISCARDED subscriptions). The expiry job is queued before any access is granted, so a failure to queue grants nothing. Job ID admin-member-expiry-<adminUser>-<sub>; an earlier one for the same pair is replaced.
  • Remove membership: deletes siem_auths rows for that admin user and subscription with isAdminMember true.
  • Detach owner policy: removes the admin user's link to that subscription's Tenant-Owner-Default policy.
  • Delete boundaries: deletes every IAM user boundary of the admin user, not only those for this subscription (errors ignored).
  • Reset workspace: if the admin's currentSubscriptionId is this subscription, it moves to another active membership or null. Then IAM and auth caches are cleared so access ends at once.
Source evidence · pinned commitsrc/bullmq/workers/lifecycleWorker.ts:211 ↗
Live job typeArtifact + browser checks passed

task:sub-finalization

Finalizes ended trials and cancelled paid periods when the original timer did not finish the work.

Runs: Every day at midnight.

  1. Find ended periodstrial or cancel flag
  2. Exclude final statesnot CANCELLED/ENDED/EXPIRED
  3. Paid or trial?branch by trial flag
  4. Start remaining gracewarnings + final job
  5. Notify customeremail + in-app

Stops / waits: Nothing due (no eligible ended period); Log row failure (continue other rows).

Exact behavior, writes, messages and failure handling

task:sub-finalization

  • Daily midnight: past currentPeriodEnd, trial or cancel flag, excluding CANCELLED/ENDED/EXPIRED. repeatableWorker.ts:684–708; repeatableQueue.ts:75–79.
  • Paid: beginCancellationGracePeriod → CANCELLED, normal interval grace, warnings/final, Your Paid Access Has Ended email; then Subscription Period Ended push. Query also admits other nonterminal statuses with cancel flag. repeatableWorker.ts:721–741; subscription.service.ts:2472–2500.
  • Trial: require trialEnd passed; ENDED, isTrialActive false, expiryDate = trialEnd +7 days. Schedule remaining grace/warnings; trial-ended email and push. repeatableWorker.ts:744–769.
  • Row and push errors are caught locally; remaining subscriptions continue. Safety net for lost trial/cancellation finalization, no renewal charge. repeatableWorker.ts:771–776.

Reminders and wallet messages

3 job types
Live job typeArtifact + browser checks passed

subscription-reminder (email-jobs)

Builds a reminder from current subscription state when it is due; skips stale dates.

Triggered by: Reminders rebuilt — updateScheduledReminders

  1. Job fires7, 3, 1 days before
  2. Date + status same?rendered now
  3. Trial?PENDING_SETUP / TRIALING
  4. Set to cancel?cancelAtPeriodEnd
  5. Card on file?amount > 0
  6. Renewal noticewith exact amount

Stops / waits: Not sent (date moved or status left).

Exact behavior, writes, messages and failure handling

subscription-reminder (email-jobs)

  • Queued by SubscriptionService.updateScheduledReminders, called at: free claim, admin activation, renewal settlement, upgrade with a new period, cancel, resume, trial conversion and task:reminder-sync (03:00). It removes the old three jobs and adds rem-7d-<sub>, rem-3d-<sub>, rem-1d-<sub> at 7/3/1 days before the target date; times already past are not added; subscriptions.emailReminders lists what was added (for example ["7d","3d","1d"]).
  • Target date: trialEnd for a trial in PENDING_SETUP or TRIALING; currentPeriodEnd for ACTIVE; none for any other status (all three removed).
  • When the job runs, the email is built from the subscription as it is now. If the target date is no longer the one the job was queued for, or the status left, nothing is sent (logged 'Reminder no longer applies').
  • Trial → 'Your <plan> Trial Ends in N Days – Upgrade Now'. Cancel flag → 'Your <plan> Plan Expires in N Days' with 'Resume Subscription'. No card and amount > 0 → 'Action Required: Add a Payment Method Before <date>'. Otherwise 'Your <plan> Plan Renews in N Days' with the amount that will be charged (scheduled downgrade plan, pushed price and running discount included), or 'Your discount covers this renewal, so no payment is taken.'
  • attempts 3 with backoff; a failed job is kept for 1 day.
Source evidence · pinned commitsrc/services/subscription.service.ts:323 ↗
Live job typeArtifact + browser checks passed

send-email (wallet emails on email-jobs)

Delivers queued wallet purchase confirmations and low-credit alerts.

Triggered by: Top-up completed — wallet-purchase-<id>

  1. Job firesready HTML in job
  2. Send via SMTPsendRawEmail
  3. Delivered?success flag

Stops / waits: Throw, retry (attempts 3).

Exact behavior, writes, messages and failure handling

send-email (wallet emails on email-jobs)

  • Top-up completed: handleTopupSucceeded → queuePurchaseConfirmation adds job wallet-purchase-<purchaseId> (fixed ID, deduplicated while its retained job ID exists) with 'Wallet Credit Purchase Confirmation - <org>': amount paid in the purchase currency, credits added, gateway, transaction ID, available balance, totals. An in-app notification is written at the same time. wallet_purchases.confirmationQueuedAt is set once queued; a missed one is re-queued by task:wallet-coupon-expiry after 2 minutes.
  • Debit crosses below the threshold (balance before ≥ threshold, after < threshold): debitWallet adds job wallet-low-balance-<wallet>-<time> with 'Alert: Low Wallet Credit Balance - <org>' plus an in-app notification. Threshold = last credit added × alert %.
  • The worker sends the HTML as stored in the job. A failed send throws; BullMQ retries 3 times with backoff.
  • No receipt PDF is attached to the purchase confirmation.
Source evidence · pinned commitsrc/email/email.service.ts:2644 ↗
Live job typeArtifact + browser checks passed

task:reminder-sync

Rebuilds missing reminder metadata and timers for eligible subscriptions.

Runs: Every day at 3:00 AM.

  1. Find missing remindersemailReminders = DbNull
  2. Eligible status?ACTIVE/TRIALING/PENDING_SETUP
  3. Rebuild reminder setupdateScheduledReminders
  4. Replace 7/3/1-day jobsfuture dates only
  5. Record queued labelsemailReminders

Stops / waits: Nothing missing (already recorded); Log row failure (continue remaining rows).

Exact behavior, writes, messages and failure handling

task:reminder-sync

  • Daily 03:00; select ACTIVE/TRIALING/PENDING_SETUP with database-null emailReminders. Empty or partial arrays are not selected. repeatableWorker.ts:637–652; repeatableQueue.ts:68–72.
  • updateScheduledReminders removes old jobs, chooses trialEnd or currentPeriodEnd, queues future 7/3/1-day reminders and records labels. subscription.service.ts:390–441.
  • Delivery rereads state and skips stale targets. subscription.service.ts:323–350; emailWorker.ts:16–24.
  • Per-row errors logged; next daily sweep sees still-null rows. This job does not detect Redis timers lost after reminder metadata was recorded, and sends no email itself. repeatableWorker.ts:668–680.

Reservations and unpaid checkout cleanup

4 job types
Live job typeArtifact + browser checks passed

task:coupon-booking-maintenance (every minute)

Expires idle selections, releases unattached reservations and recovers checkout/settlement follow-ups.

Runs: Every minute.

  1. Job fires100 rows per step
  2. Idle selectionspast expiresAt
  3. Unattached slotsno checkout yet
  4. Checkout slots duenextExpiryCheckAt passed
  5. Unfinished follow-upssettled, not done
Exact behavior, writes, messages and failure handling

task:coupon-booking-maintenance (every minute)

  • Repeatable job coupon-booking-maintenance, every minute; attempts 3.
  • Idle selections: discount_selections still ACTIVE whose expiresAt passed (35 minutes after the last selection call, 24 hours at most) → EXPIRED.
  • Unattached slots: coupon_redemptions BOOKED with no subscription (reserved by a selection that never reached checkout) and bookingExpiresAt passed → RELEASED, coupons.bookingCount − 1, under the coupon row lock.
  • Checkout slots due: BOOKED rows attached to a signup subscription whose nextExpiryCheckAt passed → nextExpiryCheckAt moved 5 minutes ahead (so only one worker takes it) and task:coupon-booking-expire queued (flow 14).
  • Unfinished follow-ups: invoices with settlementApplied true and settlementFollowUpAt empty whose next check time passed → claimed for 5 minutes and task:payment-settlement-follow-up queued (flow 18). This is how a lost or failed follow-up (emails, next renewal job) is retried.
Source evidence · pinned commitsrc/services/couponBooking.service.ts:151 ↗
Live job typeArtifact + browser checks passed

task:coupon-booking-expire

Closes a due coupon checkout only after checking gateway payment state and locking its records.

Triggered by: Maintenance every minute — bookingId; attempts 3

  1. Load coupon bookingBOOKED and attached
  2. Signup eligible?cycle 0, unpaid, open
  3. Due and idle?no debit in flight
  4. Ask payment gatewaycloseSignupCheckout
  5. Close under row locksrecheck before writing

Stops / waits: Skip missing booking (released / redeemed); Skip finished signup (paid, closed, later cycle); Wait for next sweep (not due / debit active).

Exact behavior, writes, messages and failure handling

task:coupon-booking-expire

  • Producer: enqueueExpired claims due attached BOOKED rows for five minutes, then adds coupon-booking-expire-<bookingId>. couponBooking.service.ts:151–179; repeatableWorker.ts:53–55.
  • Stop if booking is missing, not BOOKED or unattached; subscription is absent, closed, past cycle 0 or no longer UNPAID/PAYMENT_FAILED; debit is in flight; or deadline is still ahead. couponBooking.service.ts:183–188.
  • Gateway branches: captured Razorpay payment → reconcileCapturedPayment and settle; pending → return for a later maintenance pass; lateAuthorisationUntil → move nextExpiryCheckAt to that time; otherwise closeSignup. Pending and late authorization do not release the slot. couponBooking.service.ts:189–202.
  • CloseSignup locks coupon then subscription, rechecks flight and status, stamps checkoutClosedAt, releases the attached slot and decrements bookingCount. No email here. couponBooking.service.ts:215–243.
Live job typeArtifact + browser checks passed

task:wallet-coupon-expiry

Expires abandoned wallet coupon reservations and recovers missed confirmation enqueueing.

Runs: Every five minutes.

  1. Find abandoned couponspending / failed >15 min
  2. Expire atomicallyupdate if still open
  3. Release coupon slotonly if update succeeded
  4. Find missed confirmationscompleted >2 min
  5. Queue purchase emailup to 100 purchases

Stops / waits: Skip changed purchase (settled while scanned); Log per-row error (next pulse can retry).

Exact behavior, writes, messages and failure handling

task:wallet-coupon-expiry

  • Every five minutes cleanupExpiredReservations(15), then recoverPurchaseConfirmations. repeatableWorker.ts:1062–1072; repeatableQueue.ts:126–129.
  • Expire only pending/failed purchases with coupon, created >15 minutes ago; coupon-free purchases are untouched. Transaction updates only still-open status, then releases slot only if update succeeded. wallet-topup-expiry.service.ts:10–55.
  • Per-purchase expiry errors are logged and swallowed; the next pulse revisits eligible rows. wallet-topup-expiry.service.ts:56–63.
  • Recover up to 100 completed purchases with confirmationQueuedAt null and updatedAt >2 minutes ago. Queue fixed wallet-purchase-<id>, record confirmationQueuedAt. Missing owner email is also marked finished. wallet-payment-event.service.ts:183–245.
  • This repairs missed email enqueueing, not settlement polling or delivery jobs that exhausted attempts.
Live job typeArtifact + browser checks passed

task:abandoned-cleanup

Deletes old unpaid signups while preserving in-flight, coupon-held and late-payable checkouts.

Runs: Every day at 12:15 AM.

  1. Find old unpaid signupscreated >25 hours ago
  2. Check coupon and flightno debit; no open coupon
  3. Ask Razorpay if neededcaptured or still payable?
  4. Lock and re-read rowstatus + booking + flight
  5. Delete unpaid signupsubscription row

Stops / waits: Nothing to clean (UNPAID/PAYMENT_FAILED); Keep unsafe signup (payable / changed / booking); Log row exception (continue next row).

Exact behavior, writes, messages and failure handling

task:abandoned-cleanup

  • Daily 00:15. Select UNPAID/PAYMENT_FAILED created >25 hours ago, signupPaymentInFlight false, and coupon null or checkoutClosedAt set. repeatableWorker.ts:568–590; repeatableQueue.ts:61–65.
  • Razorpay gatewayState: captured → reconcile and keep; checkout still payable → keep for late authorization. repeatableWorker.ts:603–616.
  • Under subscription lock re-read status, flight and coupon booking. Retain an open/BOOKED coupon signup, active debit or changed status; otherwise delete. repeatableWorker.ts:618–627.
  • Per-row exceptions logged and swallowed. No email, refund or charge. repeatableWorker.ts:628–634.

Security and system alerts

2 job types
Live job typeArtifact + browser checks passed

task:severity-alerts

Matches severe events to assets, checks recipient permissions and sends deduplicated CVE/event alerts.

Runs: Every five minutes.

  1. Read severity eventslevel 12–16; max 500
  2. Map to active assetID / engine_id / source IP
  3. Gate subscription + userACTIVE/TRIALING; IAM
  4. Group CVE or single eventmuted rules excluded
  5. Dedupe then notifyin-app first; new → email

Stops / waits: No matching events (five-minute window); Skip disallowed recipient (IAM / preference / resource); Duplicate suppressed (same notice within 4.5 min).

Exact behavior, writes, messages and failure handling

task:severity-alerts

  • Every five minutes query OpenSearch at job.timestamp, preceding five-minute window, rule.level 12–16, maximum 500 hits. repeatableQueue.ts:49–53; repeatableWorker.ts:82–92; cron/severityEmail.ts:387–405,498–518.
  • Resolve active assets by agent ID; agentless 000 by SNMP engine_id or Syslog location/IP. Keep ACTIVE/TRIALING subscriptions. repeatableWorker.ts:94–174,204–216,398–413.
  • Owner + active members, deduplicated: require siem:notification:receiveseverityalerts and agent resource permission; honor email alert switches and muted global/agent rules. Agentless members also require siem:agentless:read. repeatableWorker.ts:234–323,420–481.
  • CVE grouped per agent and removed from normal loop; permitted recipient receives unmuted CVEs in one message. Normal events get orange/red messages. repeatableWorker.ts:182–381,398–552.
  • Notification dedupe compares user/title/message/type within 4.5 minutes. Email only for a new notification. Per-CVE-group and per-hit exceptions logged without failing entire pulse. alert.service.ts:467–505; repeatableWorker.ts:349–389,526–555.
Live job typeArtifact + browser checks passed

task:system-alerts

Checks disconnections and resource metrics against each recipient’s preferences and sends alerts.

Runs: Every five minutes.

  1. Collect global pulsemetrics, logs, Wazuh status
  2. Join issues to assetsdisconnect / CPU/RAM/disk
  3. Gate subscription + userACTIVE/TRIALING; IAM
  4. Apply agent preferencesthreshold + email switch
  5. Dedupe then notifysystem template + in-app

Stops / waits: Catch pulse exception (log; return completed); Skip disallowed recipient (policy / agent resource); No qualifying issue (toggle off / under threshold); Duplicate suppressed (4.5-minute notice key).

Exact behavior, writes, messages and failure handling

task:system-alerts

  • Every five minutes collect metrics/disconnect logs over preceding five minutes and Wazuh agent status (excluding 000, limit 5000). repeatableWorker.ts:561–565; repeatableQueue.ts:56–60; alert.service.ts:177–203.
  • Merge disconnected status, logs and CPU/RAM/disk issues; join active assets, process ACTIVE/TRIALING; owner and active members. alert.service.ts:205–290.
  • Require siem:notification:receivesystemalerts and matching agent resource, email alerts enabled. Per-agent settings override user thresholds. Disconnect needs its toggle; numeric metric must be truthy, ≥ threshold and <100. Exactly 100% is excluded by the code. alert.service.ts:364–442.
  • Create notification before email; duplicates within 4.5 minutes skip email. Subjects identify agent and disconnect/metric. alert.service.ts:292–361,467–505.
  • Outer exceptions are logged without rethrowing; BullMQ can show completed after a failed pulse. alert.service.ts:453–465.

Reports — one job per schedule

4 job types
Live job typeArtifact + browser checks passed

task:send-daily-reports

Emails a permitted recipient a PDF covering the previous 24 hours.

Runs: Every day at 1:00 AM.

  1. Load live subscriptionsACTIVE or TRIALING
  2. Choose owner + membersactive; deduplicated
  3. Gate report permissionsiem:report:read + daily
  4. Restrict chosen agentsIAM ∩ saved selection
  5. Generate PDF and sendrecipient-specific report

Stops / waits: Other status excluded (no report generated); Skip denied type (scope denied / no grant); Skip disabled or no agents (email / report preference).

Exact behavior, writes, messages and failure handling

task:send-daily-reports

  • Schedule 0 1 * * * (Every day at 01:00), no explicit timezone. Separate job name even though four schedules share a handler. repeatableQueue.ts:92–96; repeatableWorker.ts:887–893.
  • ACTIVE/TRIALING; owner and active members deduplicated. Require siem:report:read with allowed daily scope not denied. repeatableWorker.ts:897–952.
  • Read active agent-type assets, constrain by allowed/denied resources, skip no permitted agents. Honor saved daily enablement, global email switch, chosen agents intersected with IAM. repeatableWorker.ts:954–1028.
  • generateReportPdf and collectReportData take subscription, daily and selected/permitted agents. Window: previous 24 hours. Quarterly is rolling 90 days, not calendar-quarter boundaries. reports.service.ts:170–200.
  • sendReportEmail includes PDF, date range, summary, top IPs/countries and agent data. Thrown errors caught per subscription; others continue. Count increments after awaited send without checking a success flag in this handler. repeatableWorker.ts:1034–1059.
Live job typeArtifact + browser checks passed

task:send-weekly-reports

Emails a permitted recipient a PDF covering the previous seven days.

Runs: Every Sunday at 1:30 AM.

  1. Load live subscriptionsACTIVE or TRIALING
  2. Choose owner + membersactive; deduplicated
  3. Gate report permissionsiem:report:read + weekly
  4. Restrict chosen agentsIAM ∩ saved selection
  5. Generate PDF and sendrecipient-specific report

Stops / waits: Other status excluded (no report generated); Skip denied type (scope denied / no grant); Skip disabled or no agents (email / report preference).

Exact behavior, writes, messages and failure handling

task:send-weekly-reports

  • Schedule 30 1 * * 0 (Sunday at 01:30), no explicit timezone. Separate job name even though four schedules share a handler. repeatableQueue.ts:99–103; repeatableWorker.ts:887–893.
  • ACTIVE/TRIALING; owner and active members deduplicated. Require siem:report:read with allowed weekly scope not denied. repeatableWorker.ts:897–952.
  • Read active agent-type assets, constrain by allowed/denied resources, skip no permitted agents. Honor saved weekly enablement, global email switch, chosen agents intersected with IAM. repeatableWorker.ts:954–1028.
  • generateReportPdf and collectReportData take subscription, weekly and selected/permitted agents. Window: previous seven days. Quarterly is rolling 90 days, not calendar-quarter boundaries. reports.service.ts:170–200.
  • sendReportEmail includes PDF, date range, summary, top IPs/countries and agent data. Thrown errors caught per subscription; others continue. Count increments after awaited send without checking a success flag in this handler. repeatableWorker.ts:1034–1059.
Live job typeArtifact + browser checks passed

task:send-monthly-reports

Emails a permitted recipient a PDF covering the previous month from execution time.

Runs: At 2:00 AM on the first day of every month.

  1. Load live subscriptionsACTIVE or TRIALING
  2. Choose owner + membersactive; deduplicated
  3. Gate report permissionsiem:report:read + monthly
  4. Restrict chosen agentsIAM ∩ saved selection
  5. Generate PDF and sendrecipient-specific report

Stops / waits: Other status excluded (no report generated); Skip denied type (scope denied / no grant); Skip disabled or no agents (email / report preference).

Exact behavior, writes, messages and failure handling

task:send-monthly-reports

  • Schedule 0 2 1 * * (Month starts at 02:00), no explicit timezone. Separate job name even though four schedules share a handler. repeatableQueue.ts:106–110; repeatableWorker.ts:887–893.
  • ACTIVE/TRIALING; owner and active members deduplicated. Require siem:report:read with allowed monthly scope not denied. repeatableWorker.ts:897–952.
  • Read active agent-type assets, constrain by allowed/denied resources, skip no permitted agents. Honor saved monthly enablement, global email switch, chosen agents intersected with IAM. repeatableWorker.ts:954–1028.
  • generateReportPdf and collectReportData take subscription, monthly and selected/permitted agents. Window: previous month from execution. Quarterly is rolling 90 days, not calendar-quarter boundaries. reports.service.ts:170–200.
  • sendReportEmail includes PDF, date range, summary, top IPs/countries and agent data. Thrown errors caught per subscription; others continue. Count increments after awaited send without checking a success flag in this handler. repeatableWorker.ts:1034–1059.
Live job typeArtifact + browser checks passed

task:send-quarterly-reports

Emails a permitted recipient a PDF covering the previous 90 days.

Runs: At 3:00 AM on January 1, April 1, July 1, and October 1.

  1. Load live subscriptionsACTIVE or TRIALING
  2. Choose owner + membersactive; deduplicated
  3. Gate report permissionsiem:report:read + quarterly
  4. Restrict chosen agentsIAM ∩ saved selection
  5. Generate PDF and sendrecipient-specific report

Stops / waits: Other status excluded (no report generated); Skip denied type (scope denied / no grant); Skip disabled or no agents (email / report preference).

Exact behavior, writes, messages and failure handling

task:send-quarterly-reports

  • Schedule 0 3 1 */3 * (Quarter schedule at 03:00), no explicit timezone. Separate job name even though four schedules share a handler. repeatableQueue.ts:113–117; repeatableWorker.ts:887–893.
  • ACTIVE/TRIALING; owner and active members deduplicated. Require siem:report:read with allowed quarterly scope not denied. repeatableWorker.ts:897–952.
  • Read active agent-type assets, constrain by allowed/denied resources, skip no permitted agents. Honor saved quarterly enablement, global email switch, chosen agents intersected with IAM. repeatableWorker.ts:954–1028.
  • generateReportPdf and collectReportData take subscription, quarterly and selected/permitted agents. Window: previous 90 days. Quarterly is rolling 90 days, not calendar-quarter boundaries. reports.service.ts:170–200.
  • sendReportEmail includes PDF, date range, summary, top IPs/countries and agent data. Thrown errors caught per subscription; others continue. Count increments after awaited send without checking a success flag in this handler. repeatableWorker.ts:1034–1059.

Record cleanup and maintenance

3 job types
Live job typeArtifact + browser checks passed

task:maintenance-cleanup

Removes leftover local assets from expired subscriptions and tells admins what needs remote removal.

Runs: Every day at 2:30 AM.

  1. Find EXPIRED rowsperiod end >3 days ago
  2. Load remaining assetsskip empty subscriptions
  3. Log then delete assetassetLog + assets.delete
  4. Notify adminsremoved agent list
  5. Finish safety sweepno status transition

Stops / waits: No remaining agents (skip subscription); Keep failed asset (log; continue).

Exact behavior, writes, messages and failure handling

task:maintenance-cleanup

  • Query selects only EXPIRED with currentPeriodEnd >3 days ago, despite broad log wording; not CANCELLED. repeatableWorker.ts:780–805; repeatableQueue.ts:85–89.
  • Each asset gets REMOVED log then local delete. Individual errors are caught; next sweep can retry remaining rows. repeatableWorker.ts:813–840.
  • If any removed, admins receive maintenance safety-sweep removal email and SYSTEM push. repeatableWorker.ts:842–880.
  • No Wazuh deletion, customer email or subscription status transition here. Admin must remove remote agents. repeatableWorker.ts:780–884.
Live job typeArtifact + browser checks passed

task:activity-log-cleanup

Deletes activity logs older than 90 days.

Runs: Every day at midnight.

  1. Capture execution timenew Date()
  2. Calculate retention cutofftoday minus 90 days
  3. Delete old activity logscreatedAt < cutoff
  4. Log deleted countcomplete; no email

Stops / waits: Database failure → throw (three queue attempts).

Exact behavior, writes, messages and failure handling

task:activity-log-cleanup

  • Daily midnight schedule, ID activity-log-cleanup. repeatableQueue.ts:36–40.
  • Cutoff 90 days before execution; activityLog.deleteMany createdAt strictly less than cutoff. Exact-cutoff rows are retained. Log deleted.count. repeatableWorker.ts:69–80.
  • No gateway, subscription write or email. Thrown DB error reaches outer rethrow; three attempts with exponential backoff from two seconds. repeatableWorker.ts:1084–1090; repeatableQueue.ts:6–11.
Live job typeArtifact + browser checks passed

task:otp-cleanup

Deletes expired OTP records.

Runs: Every day at 2:00 AM.

  1. Job startsotp-cleanup
  2. Delete expired OTPsexpiresAt ≤ now
  3. Read deletion countrepository result
  4. Log and completeno customer message

Stops / waits: Database failure → throw (three attempts + backoff).

Exact behavior, writes, messages and failure handling

task:otp-cleanup

  • Daily 02:00 schedule, ID otp-cleanup. repeatableQueue.ts:30–34.
  • deleteExpiredOtps deletes expiresAt less than or equal to execution time and returns count; handler logs it. repeatableWorker.ts:62–66; otp.repository.ts:72–80.
  • No payment or email. Repository errors bubble to repeatable worker rethrow and three-attempt queue policy. repeatableWorker.ts:1084–1090; repeatableQueue.ts:6–11.

Unused, placeholder and retired paths

5 paths
Inactive pathArtifact + browser checks passed

task:grace-start — unused scheduler

Unused cancellation scheduler; the handler still exists for a hypothetical or old queued job.

Triggered by: No current caller — scheduleGraceStartJob only

  1. Hypothetical job runsgrace-start-<sub>
  2. Load subscriptionmissing → skip
  3. ACTIVE + cancel flag?no period-end check here
  4. Begin cancellation graceCANCELLED + warning/final

Stops / waits: Skip missing row (nothing to finalize); Skip other status (or no cancel flag).

Exact behavior, writes, messages and failure handling

task:grace-start — unused scheduler

  • Repository search finds scheduleGraceStartJob declaration but no runtime caller. This is unused, not a live cancellation timer. lifecycleQueue.ts:103–111.
  • An old/manual job loads subscription then starts grace only if ACTIVE with cancelAtPeriodEnd; no period-end check in this handler. lifecycleWorker.ts:194–208.
  • Live cancellation is performed by billing-cycle and sub-finalization. billingCycleWorker.ts:166–170; repeatableWorker.ts:721–741. No source removed.
Inactive pathArtifact + browser checks passed

readymade-email — unused producer

Unused ready-HTML producer; live wallet email jobs use a different name.

Triggered by: No current caller — scheduleReadymadeEmail only

  1. Hypothetical enqueueto / subject / html
  2. Delay until sendAtreadymade-email
  3. Send stored HTMLsendRawEmail
  4. Finish or throwdelivery result checked

Stops / waits: Delivery failure → retry (three attempts + backoff).

Exact behavior, writes, messages and failure handling

readymade-email — unused producer

  • scheduleReadymadeEmail declaration has no runtime caller. It adds readymade-email with provided recipient/subject/HTML delayed to sendAt. emailQueue.service.ts:16–32.
  • Worker treats non-subscription-reminder names as ready HTML and calls sendRawEmail. Unsuccessful result throws. emailWorker.ts:14–42.
  • Three attempts, exponential backoff from two seconds; helper overrides retention. Live wallet emails use send-email. emailQueue.ts:4–12; emailQueue.service.ts:23–31.
Inactive pathArtifact + browser checks passed

provisioning-jobs — no current producer

A provisioning worker exists, but no current source code queues work for it.

Triggered by: No current enqueue site — queue and worker exist

  1. Read job payloaduserId / subId / companyName
  2. Create remote orgmiddleware addOrg
  3. Record local membershipsiemAuth + designation
  4. Sync timezoneremote org settings
  5. Log success or throwno status activation

Stops / waits: Retry thrown error (25 attempts; 1 min backoff).

Exact behavior, writes, messages and failure handling

provisioning-jobs — no current producer

  • Queue and worker exist, but repository search found no provisioningQueue.add or task:provision-org producer. Worker accepts any name rather than dispatching a registered job type. provisioningQueue.ts:4–17; provisioningWorker.ts:6–18.
  • If manually/previously enqueued, call addNewOrg: create remote org, record local owner membership/designation, clear designation, sync timezone. No ACTIVE status write. middlewareApi.ts:118–164.
  • Rethrow failures; 25 attempts, exponential backoff starting 60 seconds, concurrency two. provisioningQueue.ts:6–15; provisioningWorker.ts:19–33.
  • Paid onboarding calls addNewOrg inside settlement follow-up if membership missing; free claim creates/records org in its own flow. PaymentEventProcessor.ts:776–781; subscription.service.ts:526–554.
Inactive pathArtifact + browser checks passed

notification-jobs — logging placeholder

A queue and worker exist, but the worker only logs; it delivers no notifications.

Triggered by: No current enqueue site — no named task registered

  1. Receive hypothetical jobany name / payload
  2. Log ID and nameconsole.log only
  3. Worker completesno delivery implementation
  4. Completion event logsnot notification dispatch
Exact behavior, writes, messages and failure handling

notification-jobs — logging placeholder

  • Worker logs ID/name and returns; completed/failed listeners only log. notificationWorker.ts:4–19.
  • Queue exists on Bull Board, but search found no notificationQueue.add producer. notificationQueue.ts:4–12; dashboard/bullBoard.ts:52.
  • Actual notifications/webpush go through services/utilities, not this queue worker. alert.service.ts:467–544. Placeholder, not a working notification task.
Inactive pathArtifact + browser checks passed

task:stripe-event-cleanup — retired

Startup removes a retired repeatable definition instead of scheduling a cleanup job.

Triggered by: Scheduler startup — remove legacy schedule

  1. Initialize schedulesinitRepeatableJobs
  2. Remove old repeatablename + cron + jobId
  3. No handler registeredStripeEventLog removed
  4. Current webhook logs staynot pruned by this task
Exact behavior, writes, messages and failure handling

task:stripe-event-cleanup — retired

  • Retired, not a current cron job. Startup removes task:stripe-event-cleanup with pattern 0 0 * * * and ID stripe-event-cleanup. repeatableQueue.ts:43–44.
  • No matching worker case; unknown names only warn. Removing repeatable definition does not prove every already-materialized historical Redis job is deleted. repeatableWorker.ts:1081–1082.
  • This retired timer does not prune current webhook logs. No Redis connection used to inspect historical jobs.