Documentation
¶
Overview ¶
Package app (piece ⑦) holds every business operation of Exhale. The web, the chat adapters, the scheduler and AI agents all call the same use cases (ADR 0003, 0018).
Index ¶
- Constants
- Variables
- func CollectAudits(ctx context.Context) (context.Context, *[]AuditRef)
- func DayRuleName(r engine.Rule) string
- func DuplicateKey(subs []SubscriptionView) string
- func NormalizeLogo(data []byte) ([]byte, error)
- func ShiftName(r engine.Rule) string
- func SnoozeDay(kind string, c ChargeView, today engine.Date, days int) (on engine.Date, clamped, ok bool)
- func SnoozeLimit(kind string, c ChargeView) (engine.Date, bool)
- type Actor
- type ActorKind
- type AddDocumentInput
- type AgentContext
- type AttentionList
- type AuditEntry
- type AuditLog
- type AuditLogInput
- type AuditRef
- type AuditReport
- type Balances
- type BalancesInput
- type Binding
- type BootstrapInput
- type Breakdown
- type BreakdownRow
- type BulkItem
- type BulkResult
- type BulkUpdateInput
- type CalendarEvent
- type CalendarInput
- type CalendarView
- type CardImpact
- type CardNotice
- type CategoryOut
- type CategorySpend
- type ChangeChargeAmountInput
- type ChangeChargeDateInput
- type ChargeList
- type ChargeMatch
- type ChargeResult
- type ChargeView
- type CheaperPlan
- type CreateCategoryInput
- type CreateSubscriptionInput
- type DecideRenewalInput
- type DeleteCategoryInput
- type DeleteDocumentInput
- type DeletePaymentMethodInput
- type DeleteResult
- type DeleteSubscriptionInput
- type DeleteTagInput
- type DeliveryProblem
- type DismissDuplicateInput
- type DismissResult
- type Document
- type DocumentList
- type DocumentResult
- type DuplicateGroup
- type DuplicateNotice
- type ExportData
- type ExportedSubscription
- type ExtrasInput
- type ExtrasUpdate
- type FieldChange
- type GetSubscriptionInput
- type Glance
- type GlanceCharge
- type GlanceInput
- type HouseholdInfo
- type ImportInput
- type ImportItem
- type ImportOutcome
- type ImportResult
- type KeepAwake
- type KeepAwakeInfo
- type KeepAwakePower
- type KeepAwakeSession
- type KeepAwakeSessionInput
- type KeepAwakeSetting
- type KeepAwakeStatus
- type ListChargesInput
- type ListDocumentsInput
- type ListSubscriptionsInput
- type LogoResult
- type MatchStatementInput
- type MemberBalance
- type Meta
- type MethodSpend
- type MonthSpend
- type Op
- type OutboxItem
- type PaidTotals
- type PaydayOutlook
- type PaymentMethodOut
- type PlanSwitch
- type PreviewScheduleInput
- type PriceChangeHint
- type PriceIncrease
- type PricePoint
- type ReassignPaymentMethodInput
- type ReassignResult
- type ReminderEntry
- type ReminderLog
- type ReminderLogInput
- type RemoveLogoInput
- type RenameTagInput
- type SavePaymentMethodInput
- type SchedulePreview
- type Scope
- type Service
- func (s *Service) ActorForMember(ctx context.Context, memberID string) (Actor, error)
- func (s *Service) AddDocument(ctx context.Context, actor Actor, in AddDocumentInput) (DocumentResult, error)
- func (s *Service) Audit(ctx context.Context, actor Actor, _ struct{}) (AuditReport, error)
- func (s *Service) Bootstrap(ctx context.Context, in BootstrapInput) (Actor, error)
- func (s *Service) BuildContext(ctx context.Context, actor Actor, _ struct{}) (AgentContext, error)
- func (s *Service) BulkUpdate(ctx context.Context, actor Actor, in BulkUpdateInput) (BulkResult, error)
- func (s *Service) Bus() *bus.Bus
- func (s *Service) Calendar(ctx context.Context, actor Actor, in CalendarInput) (CalendarView, error)
- func (s *Service) Card(ctx context.Context, actor Actor, pmID string) (CardNotice, bool, error)
- func (s *Service) ChangeChargeAmount(ctx context.Context, actor Actor, in ChangeChargeAmountInput) (ChargeResult, error)
- func (s *Service) ChangeChargeDate(ctx context.Context, actor Actor, in ChangeChargeDateInput) (ChargeResult, error)
- func (s *Service) CreateCategory(ctx context.Context, actor Actor, in CreateCategoryInput) (domain.Category, error)
- func (s *Service) CreateSubscription(ctx context.Context, actor Actor, in CreateSubscriptionInput) (SubscriptionDetail, error)
- func (s *Service) DecideRenewal(ctx context.Context, actor Actor, in DecideRenewalInput) (ChargeResult, error)
- func (s *Service) DeleteCategory(ctx context.Context, actor Actor, in DeleteCategoryInput) (DeleteResult, error)
- func (s *Service) DeleteDocument(ctx context.Context, actor Actor, in DeleteDocumentInput) (DocumentResult, error)
- func (s *Service) DeletePaymentMethod(ctx context.Context, actor Actor, in DeletePaymentMethodInput) (ReassignResult, error)
- func (s *Service) DeleteSubscription(ctx context.Context, actor Actor, in DeleteSubscriptionInput) (DeleteResult, error)
- func (s *Service) DeleteTag(ctx context.Context, actor Actor, in DeleteTagInput) (BulkResult, error)
- func (s *Service) DismissDuplicate(ctx context.Context, actor Actor, in DismissDuplicateInput) (DismissResult, error)
- func (s *Service) DocumentFile(ctx context.Context, actor Actor, id string) (Document, []byte, error)
- func (s *Service) DueOutbox(ctx context.Context, limit int) ([]OutboxItem, error)
- func (s *Service) Duplicate(ctx context.Context, actor Actor, key string) (DuplicateNotice, bool, error)
- func (s *Service) Export(ctx context.Context, actor Actor, _ struct{}) (ExportData, error)
- func (s *Service) FileData(ctx context.Context, actor Actor, id string) ([]byte, error)
- func (s *Service) GetHousehold(ctx context.Context, actor Actor, _ struct{}) (HouseholdInfo, error)
- func (s *Service) GetKeepAwake(ctx context.Context, _ Actor, _ struct{}) (KeepAwakeInfo, error)
- func (s *Service) GetSubscription(ctx context.Context, actor Actor, in GetSubscriptionInput) (SubscriptionDetail, error)
- func (s *Service) Glance(ctx context.Context, actor Actor, in GlanceInput) (Glance, error)
- func (s *Service) Import(ctx context.Context, actor Actor, in ImportInput) (ImportResult, error)
- func (s *Service) KeepAwakeSession(ctx context.Context, actor Actor, in KeepAwakeSessionInput) (KeepAwakeInfo, error)
- func (s *Service) ListAudit(ctx context.Context, _ Actor, in AuditLogInput) (AuditLog, error)
- func (s *Service) ListCharges(ctx context.Context, actor Actor, in ListChargesInput) (ChargeList, error)
- func (s *Service) ListDocuments(ctx context.Context, actor Actor, in ListDocumentsInput) (DocumentList, error)
- func (s *Service) ListSubscriptions(ctx context.Context, actor Actor, in ListSubscriptionsInput) (SubscriptionList, error)
- func (s *Service) Logo(ctx context.Context, actor Actor, subscriptionID string) (Document, []byte, error)
- func (s *Service) MarkDelivered(ctx context.Context, id string) error
- func (s *Service) MarkFailed(ctx context.Context, item OutboxItem, cause error) error
- func (s *Service) MarkObsolete(ctx context.Context, item OutboxItem) error
- func (s *Service) MarkUndeliverable(ctx context.Context, item OutboxItem, cause error) error
- func (s *Service) MatchStatement(ctx context.Context, actor Actor, in MatchStatementInput) (StatementMatch, error)
- func (s *Service) MonthBalances(ctx context.Context, actor Actor, in BalancesInput) (Balances, error)
- func (s *Service) NeedsAttention(ctx context.Context, actor Actor, _ struct{}) (AttentionList, error)
- func (s *Service) NextOutboxDue(ctx context.Context) (time.Time, bool)
- func (s *Service) Now() time.Time
- func (s *Service) Owner(ctx context.Context) (Actor, error)
- func (s *Service) Plan(ctx context.Context) error
- func (s *Service) PreviewSchedule(_ context.Context, _ Actor, in PreviewScheduleInput) (SchedulePreview, error)
- func (s *Service) PurgeDeletedFiles(ctx context.Context) (int64, error)
- func (s *Service) ReassignPaymentMethod(ctx context.Context, actor Actor, in ReassignPaymentMethodInput) (ReassignResult, error)
- func (s *Service) Reminders(ctx context.Context, actor Actor, in ReminderLogInput) (ReminderLog, error)
- func (s *Service) RemoveLogo(ctx context.Context, actor Actor, in RemoveLogoInput) (LogoResult, error)
- func (s *Service) RenameTag(ctx context.Context, actor Actor, in RenameTagInput) (BulkResult, error)
- func (s *Service) SavePaymentMethod(ctx context.Context, actor Actor, in SavePaymentMethodInput) (domain.PaymentMethod, error)
- func (s *Service) SetChargeStatus(ctx context.Context, actor Actor, in SetChargeStatusInput) (ChargeResult, error)
- func (s *Service) SetKeepAwake(ctx context.Context, actor Actor, in SetKeepAwakeInput) (KeepAwakeInfo, error)
- func (s *Service) SetLogo(ctx context.Context, actor Actor, in SetLogoInput) (LogoResult, error)
- func (s *Service) SetSubscriptionStatus(ctx context.Context, actor Actor, in SetSubscriptionStatusInput) (SubscriptionDetail, error)
- func (s *Service) SetupLidClosed(ctx context.Context, actor Actor, in SetupLidClosedInput) (KeepAwakeInfo, error)
- func (s *Service) Snooze(ctx context.Context, actor Actor, in SnoozeInput) (SnoozeResult, error)
- func (s *Service) SpendBreakdown(ctx context.Context, actor Actor, period, by string, ids []string) (Breakdown, error)
- func (s *Service) SpendHistory(ctx context.Context, actor Actor, in SpendingHistoryInput) (SpendingHistory, error)
- func (s *Service) Store() *store.Store
- func (s *Service) Summary(ctx context.Context, actor Actor, _ struct{}) (SpendingSummary, error)
- func (s *Service) Undo(ctx context.Context, actor Actor, in UndoInput) (UndoResult, error)
- func (s *Service) UpdateCategory(ctx context.Context, actor Actor, in UpdateCategoryInput) (domain.Category, error)
- func (s *Service) UpdateHousehold(ctx context.Context, actor Actor, in UpdateHouseholdInput) (HouseholdInfo, error)
- func (s *Service) UpdateSubscription(ctx context.Context, actor Actor, in UpdateSubscriptionInput) (SubscriptionDetail, error)
- type SetChargeStatusInput
- type SetKeepAwakeInput
- type SetLogoInput
- type SetSubscriptionStatusInput
- type SetupLidClosedInput
- type SharedCharge
- type SnoozeInput
- type SnoozeResult
- type SpendingHistory
- type SpendingHistoryInput
- type SpendingSummary
- type StatementLine
- type StatementMatch
- type StoppedSpend
- type StoppedSubscription
- type SubscriptionDetail
- type SubscriptionList
- type SubscriptionRef
- type SubscriptionSpend
- type SubscriptionView
- type TagCount
- type TermsInput
- type Transfer
- type UndoInput
- type UndoResult
- type UnusedSubscription
- type UpdateCategoryInput
- type UpdateHouseholdInput
- type UpdateSubscriptionInput
Constants ¶
const ( KindDuplicate = "duplicate" // "You and Ana both pay for Spotify" KindCardExpiry = "card" // "Visa ••4421 expires in 20 days: 7 subscriptions use it" )
Outbox kinds of messages that are not about a charge.
const ( EventCharge = "charge" // a renewal EventTrial = "trial" // a free trial turns into the first charge EventExpiry = "expiry" // it expires unless renewed EventCancelBy = "cancelBy" // last day to cancel before a renewal )
Calendar event kinds.
const ( // MaxDocumentSize is the largest document one upload can add. MaxDocumentSize = 10 << 20 // MaxLogoUpload is the largest picture accepted as a logo. It is // re-encoded to a small PNG before it is stored. MaxLogoUpload = 2 << 20 // MaxHouseholdFiles bounds what one household keeps, deleted files of // the last 30 days included. MaxHouseholdFiles = 500 << 20 // LogoSize is the side of the square a logo is fitted into. LogoSize = 256 // FileRetention is how long a deleted file is kept, so undo can bring it // back. FileRetention = 30 * 24 * time.Hour )
Limits of files.
const ( DocContract = "contract" DocInvoice = "invoice" DocOther = "other" )
Document kinds. A logo is stored the same way but is not a document.
const ( KeepAwakeOff = "off" KeepAwakeAlways = "always" KeepAwakeHours = "hours" )
Keep-awake modes.
const ( AwakeOff = "off" // the owner turned it off AwakeActive = "active" // the machine is kept awake now AwakeWaiting = "waiting" // outside the hours AwakePaused = "paused" // on battery: not plugged in, or below the floor (see Reason) AwakeFailed = "failed" // the system refused (see Error) AwakeUnknown = "unknown" // this process does not apply it (exhale mcp) )
Keep-awake states, as the web shows them.
const ( SessionStart = "start" SessionExtend = "extend" SessionStop = "stop" )
Session actions.
const ( KindReminder = "reminder" // "Netflix renews in 3 days" KindConfirm = "confirm" // "Was Netflix charged?" KindDecision = "decision" // "Last day to cancel the gym is in 7 days: keep or cancel?" )
Outbox kinds.
const ( MatchMarkPaid = "mark_paid" MatchAlreadyPaid = "already_paid" )
Match actions.
const ( ChargeRenewal = "renewal" ChargeTrial = "trial" ChargeExpiry = "expiry" )
Charge kinds.
const ExportFormat = "exhale/1"
ExportFormat identifies Exhale's own export.
const HorizonDays = 120
HorizonDays is how far ahead charges are materialized.
const MaxAttempts = 8
MaxAttempts is how many times delivery is retried.
const MaxBulk = 500
MaxBulk bounds one bulk change (and one undo of several changes).
const MaxImportItems = 500
MaxImportItems bounds one import call.
const MaxStatementLines = 10_000
MaxStatementLines bounds one statement.
Variables ¶
var ( OpGetContext = Op[struct{}, AgentContext]{Meta{ Name: "get_context", Title: "Read the household briefing", Tag: "Context", Method: "GET", Path: "/context", Description: "Start here. Returns a live Markdown briefing: today's date and time zone, members, every subscription with ids, the next 30 days of charges, what needs attention, and the rules for changing anything safely.", }, (*Service).BuildContext} OpGetHousehold = Op[struct{}, HouseholdInfo]{Meta{ Name: "get_household", Title: "Get household reference data", Tag: "Household", Method: "GET", Path: "/household", Description: "Household settings, members, categories and payment methods with their ids.", }, (*Service).GetHousehold} OpUpdateHousehold = Op[UpdateHouseholdInput, HouseholdInfo]{Meta{ Name: "update_household", Title: "Change household preferences", Tag: "Household", Method: "POST", Path: "/household", Write: true, Idempotent: true, Description: "Changes the display currency (totals convert into it), time zone, reminder language, name, or default reminder time (HH:MM). Owners only. Also how often an unanswered \"was it charged?\" is asked again (confirmTimes, confirmEveryDays).", }, (*Service).UpdateHousehold} OpListSubscriptions = Op[ListSubscriptionsInput, SubscriptionList]{Meta{ Name: "list_subscriptions", Title: "List subscriptions", Tag: "Subscriptions", Method: "GET", Path: "/subscriptions", Description: "Lists subscriptions with price, cadence, next charge, monthly cost, category and payment method. Filter by status or name.", }, (*Service).ListSubscriptions} OpGetSubscription = Op[GetSubscriptionInput, SubscriptionDetail]{Meta{ Name: "get_subscription", Title: "Get a subscription", Tag: "Subscriptions", Method: "GET", Path: "/subscriptions/{id}", Description: "One subscription with its upcoming charges (with charge ids) and its charge history.", }, (*Service).GetSubscription} OpCreateSubscription = Op[CreateSubscriptionInput, SubscriptionDetail]{Meta{ Name: "create_subscription", Title: "Create a subscription", Tag: "Subscriptions", Method: "POST", Path: "/subscriptions", Write: true, Description: "Adds a recurring subscription and computes its upcoming charges. Also covers a free trial (trialEndsOn + the price it becomes), a notice period (noticeDays), things that expire unless renewed (renewal=manual) and subscriptions that already ended (status=cancelled). Use dryRun to preview the charge dates first.", }, (*Service).CreateSubscription} OpUpdateSubscription = Op[UpdateSubscriptionInput, SubscriptionDetail]{Meta{ Name: "update_subscription", Title: "Update a subscription", Tag: "Subscriptions", Method: "POST", Path: "/subscriptions/update", Write: true, Idempotent: true, Description: "Changes name, price (from the next open charge on, or from priceFrom for a change announced in advance), billing cycle or day (interval, unit, nextCharge: from the next charge on), end date, category, payment method, payer, cost shares, privacy, URL, notes, auto-confirm, reminders, or the contract terms (start date, trial end, notice period, renewal, cancel link, account). Only the fields you pass change.", }, (*Service).UpdateSubscription} OpSetSubscriptionStatus = Op[SetSubscriptionStatusInput, SubscriptionDetail]{Meta{ Name: "set_subscription_status", Title: "Pause, resume or cancel", Tag: "Subscriptions", Method: "POST", Path: "/subscriptions/status", Write: true, Destructive: true, Idempotent: true, Description: "Pauses, resumes or cancels a subscription. cancel_at_period_end keeps the service until the day before the next charge; cancel_now stops future charges immediately.", }, (*Service).SetSubscriptionStatus} OpDeleteSubscription = Op[DeleteSubscriptionInput, DeleteResult]{Meta{ Name: "delete_subscription", Title: "Delete a subscription", Tag: "Subscriptions", Method: "POST", Path: "/subscriptions/delete", Write: true, Destructive: true, Description: "Permanently deletes a subscription and its charges. Prefer set_subscription_status to cancel. Can be reverted with undo.", }, (*Service).DeleteSubscription} OpListCharges = Op[ListChargesInput, ChargeList]{Meta{ Name: "list_charges", Title: "List charges by date", Tag: "Charges", Method: "GET", Path: "/charges", Description: "Charges due between two dates (default: the next 30 days), with ids, amounts, status and whether they were moved.", }, (*Service).ListCharges} OpCalendar = Op[CalendarInput, CalendarView]{Meta{ Name: "calendar", Title: "Calendar of dates", Tag: "Charges", Method: "GET", Path: "/calendar", Description: "Every dated event in a period (default: the next year): charges, including the ones the schedule will produce further ahead, free trials ending, things expiring and last days to cancel.", }, (*Service).Calendar} OpNeedsAttention = Op[struct{}, AttentionList]{Meta{ Name: "needs_attention", Title: "What needs attention", Tag: "Charges", Method: "GET", Path: "/attention", Description: "Charges whose date passed without confirmation, payment methods about to expire, last days to cancel and free trials about to charge.", }, (*Service).NeedsAttention} OpChangeChargeDate = Op[ChangeChargeDateInput, ChargeResult]{Meta{ Name: "change_charge_date", Title: "Move a charge", Tag: "Charges", Method: "POST", Path: "/charges/move", Write: true, Idempotent: true, Description: "Moves a charge to another date. scope=this (default) for a one-off delay; scope=following to change the billing day from this charge on.", }, (*Service).ChangeChargeDate} OpChangeChargeAmount = Op[ChangeChargeAmountInput, ChargeResult]{Meta{ Name: "change_charge_amount", Title: "Change a charge amount", Tag: "Charges", Method: "POST", Path: "/charges/amount", Write: true, Idempotent: true, Description: "Changes the amount of a charge. scope=following (default) also changes the subscription price for later charges; scope=this only this charge.", }, (*Service).ChangeChargeAmount} OpSetChargeStatus = Op[SetChargeStatusInput, ChargeResult]{Meta{ Name: "set_charge_status", Title: "Mark a charge paid, failed or skipped", Tag: "Charges", Method: "POST", Path: "/charges/status", Write: true, Idempotent: true, Description: "Resolves a charge: paid (optionally with the amount the bank actually took, in any currency), failed, skipped, or pending to reopen it.", }, (*Service).SetChargeStatus} OpDecideRenewal = Op[DecideRenewalInput, ChargeResult]{Meta{ Name: "decide_renewal", Title: "Keep a renewal", Tag: "Charges", Method: "POST", Path: "/charges/decide", Write: true, Idempotent: true, Description: "For subscriptions with a notice period: records that the member wants to keep this renewal, so Exhale stops asking before the last day to cancel. To cancel instead, use set_subscription_status.", }, (*Service).DecideRenewal} OpPreviewSchedule = Op[PreviewScheduleInput, SchedulePreview]{Meta{ Name: "preview_schedule", Title: "Preview charge dates", Tag: "Charges", Method: "POST", Path: "/schedule/preview", Idempotent: true, Description: "Computes the charge dates of a billing rule with Exhale's engine (month-end and leap-day aware) without saving anything.", }, (*Service).PreviewSchedule} OpSummary = Op[struct{}, SpendingSummary]{Meta{ Name: "spending_summary", Title: "Spending summary", Tag: "Insights", Method: "GET", Path: "/summary", Description: "Monthly and yearly recurring spend per currency, spend per category and payment method, how much to set aside each month for yearly bills, what is due before the next payday, the charges of the next 7 days, and what cancelled subscriptions no longer cost per year (stopped).", }, (*Service).Summary} OpSpendHistory = Op[SpendingHistoryInput, SpendingHistory]{Meta{ Name: "spending_history", Title: "Real spend per month", Tag: "Insights", Method: "GET", Path: "/spending/history", Description: "What was really paid each month (from the charge ledger, not from current prices), what is expected in the coming months, and lifetime paid per subscription.", }, (*Service).SpendHistory} OpBalances = Op[BalancesInput, Balances]{Meta{ Name: "month_balances", Title: "Who owes whom", Tag: "Insights", Method: "GET", Path: "/balances", Description: "For subscriptions with cost shares: what each member paid and should carry in a month, and the fewest payments that settle it.", }, (*Service).MonthBalances} OpAudit = Op[struct{}, AuditReport]{Meta{ Name: "audit", Title: "Audit the subscriptions", Tag: "Insights", Method: "GET", Path: "/audit", Description: "Facts worth acting on, computed not guessed: duplicates, price increases in the last year, free trials about to charge, last days to cancel, things about to expire, big charges coming, cards about to expire with the subscriptions that depend on them, unconfirmed charges and subscriptions marked as not used.", }, (*Service).Audit} OpSavePaymentMethod = Op[SavePaymentMethodInput, PaymentMethodOut]{Meta{ Name: "save_payment_method", Title: "Create or update a payment method", Tag: "Household", Method: "POST", Path: "/payment-methods", Write: true, Description: "Creates a payment method (no id) or updates one (with id): name, kind, last four digits, expiry date and owner.", }, savePaymentMethod} OpReassignPaymentMethod = Op[ReassignPaymentMethodInput, ReassignResult]{Meta{ Name: "reassign_payment_method", Title: "Move subscriptions to another payment method", Tag: "Household", Method: "POST", Path: "/payment-methods/reassign", Write: true, Idempotent: true, Description: "Moves every subscription paid with one method to another in one step, e.g. when a card is about to expire.", }, (*Service).ReassignPaymentMethod} OpDeletePaymentMethod = Op[DeletePaymentMethodInput, ReassignResult]{Meta{ Name: "delete_payment_method", Title: "Delete a payment method", Tag: "Household", Method: "POST", Path: "/payment-methods/delete", Write: true, Destructive: true, Description: "Deletes a payment method. Pass moveTo so its subscriptions move to another method instead of being left without one.", }, (*Service).DeletePaymentMethod} OpUpdateCategory = Op[UpdateCategoryInput, CategoryOut]{Meta{ Name: "update_category", Title: "Rename a category", Tag: "Household", Method: "POST", Path: "/categories/update", Write: true, Idempotent: true, Description: "Renames or recolours a category.", }, updateCategory} OpDeleteCategory = Op[DeleteCategoryInput, DeleteResult]{Meta{ Name: "delete_category", Title: "Delete a category", Tag: "Household", Method: "POST", Path: "/categories/delete", Write: true, Destructive: true, Description: "Deletes a category; its subscriptions become uncategorized.", }, (*Service).DeleteCategory} OpCreateCategory = Op[CreateCategoryInput, CategoryOut]{Meta{ Name: "create_category", Title: "Create a category", Tag: "Household", Method: "POST", Path: "/categories", Write: true, Description: "Adds a category for grouping subscriptions.", }, createCategory} OpExport = Op[struct{}, ExportData]{Meta{ Name: "export_data", Title: "Export everything", Tag: "Data", Method: "GET", Path: "/export", Description: "Everything the household keeps in Exhale in one JSON document: settings, members, categories, payment methods and every subscription with all its charges.", }, (*Service).Export} OpImport = Op[ImportInput, ImportResult]{Meta{ Name: "import_subscriptions", Title: "Import subscriptions", Tag: "Data", Method: "POST", Path: "/import", Write: true, Description: "Creates many subscriptions in one step, e.g. from a bank statement, a spreadsheet or another app. Categories, payment methods and payers are given by name (missing categories and payment methods are created). Items that duplicate an existing subscription or fail validation are skipped and reported. Always call it first with dryRun and show the user the list.", }, (*Service).Import} OpReminders = Op[ReminderLogInput, ReminderLog]{Meta{ Name: "list_reminders", Title: "Reminder log", Tag: "Audit", Method: "GET", Path: "/reminders", Description: "Every reminder Exhale planned, newest first, with what happened to it: sent, scheduled, failed and why, or no longer needed. Answers \"why didn't I get that reminder?\".", }, (*Service).Reminders} OpListAudit = Op[AuditLogInput, AuditLog]{Meta{ Name: "list_changes", Title: "Recent changes", Tag: "Audit", Method: "GET", Path: "/changes", Description: "Recent changes with who made them (member, agent or system). Each id can be passed to undo.", }, (*Service).ListAudit} // Statement matching (import from statements). OpMatchStatement = Op[MatchStatementInput, StatementMatch]{Meta{ Name: "match_statement", Title: "Match a statement to expected charges", Tag: "Charges", Method: "POST", Path: "/charges/match", Idempotent: true, Description: "Compares the payments of a bank or card statement with the charges Exhale expects (merchant, date within a week, amount within 30%). Changes nothing. Returns the charges each payment confirms: for action mark_paid call set_charge_status with status=paid and the amount paid (dryRun first, then show the user); when priceChange is set, ask whether it is the new price. Also lists open charges due in the period that the statement does not show.", }, (*Service).MatchStatement} OpUndo = Op[UndoInput, UndoResult]{Meta{ Name: "undo", Title: "Undo a change", Tag: "Audit", Method: "POST", Path: "/changes/undo", Write: true, Description: "Reverts a change by its auditId, restoring the previous state.", }, (*Service).Undo} // Reminders that end in an answer. OpSnooze = Op[SnoozeInput, SnoozeResult]{Meta{ Name: "snooze_reminder", Title: "Remind me later", Tag: "Charges", Method: "POST", Path: "/charges/snooze", Write: true, Idempotent: true, Description: "Brings a reminder, a keep-or-cancel question or a was-it-charged question about a charge back in a few days at the reminder time. It never comes back too late: a reminder at the latest on the charge date, a keep-or-cancel question on the last day to cancel.", }, (*Service).Snooze} OpDismissDuplicate = Op[DismissDuplicateInput, DismissResult]{Meta{ Name: "dismiss_duplicate", Title: "A duplicate is fine", Tag: "Insights", Method: "POST", Path: "/audit/duplicates/dismiss", Write: true, Idempotent: true, Description: "Records that a service paid more than once (a group from audit's duplicates, by its key) is on purpose: audit stops reporting it and nobody is messaged about it again.", }, (*Service).DismissDuplicate} )
Use cases exposed to every adapter. Adding one here makes it available to AI agents; the parity test fails if an adapter misses one.
var ( OpBulkUpdate = Op[BulkUpdateInput, BulkResult]{Meta{ Name: "bulk_update_subscriptions", Title: "Change several subscriptions", Tag: "Subscriptions", Method: "POST", Path: "/subscriptions/bulk", Write: true, Destructive: true, Description: "Changes several subscriptions in one step, all or none: set the category or payment method, add or remove tags (action update), or pause, resume, cancel or delete them. Call it first with dryRun to show what changes for each. The auditIds it returns can all be passed to undo to revert the whole change.", }, (*Service).BulkUpdate} OpRenameTag = Op[RenameTagInput, BulkResult]{Meta{ Name: "rename_tag", Title: "Rename a tag", Tag: "Subscriptions", Method: "POST", Path: "/tags/rename", Write: true, Idempotent: true, Description: "Renames a tag on every subscription that has it; if one already has the new name, they merge. The tags in use are in get_household.", }, (*Service).RenameTag} OpDeleteTag = Op[DeleteTagInput, BulkResult]{Meta{ Name: "delete_tag", Title: "Delete a tag", Tag: "Subscriptions", Method: "POST", Path: "/tags/delete", Write: true, Destructive: true, Description: "Removes a tag from every subscription. The subscriptions stay. Can be reverted with undo (auditIds).", }, (*Service).DeleteTag} // Logos and documents (ADR 0027). Uploading and downloading a file are // plain HTTP endpoints, since they carry files, not JSON. OpListDocuments = Op[ListDocumentsInput, DocumentList]{Meta{ Name: "list_documents", Title: "List a subscription's documents", Tag: "Documents", Method: "GET", Path: "/subscriptions/{id}/documents", Description: "The files attached to a subscription (contract, terms, invoices and receipts), newest first: name, kind, size, the charge an invoice belongs to and the path to download it.", }, (*Service).ListDocuments} OpDeleteDocument = Op[DeleteDocumentInput, DocumentResult]{Meta{ Name: "delete_document", Title: "Delete a document", Tag: "Documents", Method: "POST", Path: "/documents/delete", Write: true, Destructive: true, Description: "Deletes a document attached to a subscription. It is kept 30 days and can be restored with undo.", }, (*Service).DeleteDocument} OpRemoveLogo = Op[RemoveLogoInput, LogoResult]{Meta{ Name: "remove_logo", Title: "Remove an uploaded logo", Tag: "Documents", Method: "POST", Path: "/subscriptions/logo/remove", Write: true, Idempotent: true, Description: "Drops the logo someone uploaded for a subscription: it shows the bundled brand mark or its initials again. Can be reverted with undo.", }, (*Service).RemoveLogo} // Keep this computer awake (ADR 0028, 0032). OpGetKeepAwake = Op[struct{}, KeepAwakeInfo]{Meta{ Name: "get_keep_awake", Title: "Is this computer kept awake", Tag: "Household", Method: "GET", Path: "/keep-awake", Description: "Whether Exhale keeps the computer it runs on from sleeping (so reminders go out and chat buttons answer): off, always, or between two times of day in the household time zone; whether the display may sleep; whether a closed lid, running on battery or a battery floor change that; a session started by hand; and the live status (active and until when, waiting outside the hours, paused on battery, unavailable here and why, or the error the system gave), with the battery's charge.", }, (*Service).GetKeepAwake} OpSetKeepAwake = Op[SetKeepAwakeInput, KeepAwakeInfo]{Meta{ Name: "set_keep_awake", Title: "Keep this computer awake", Tag: "Household", Method: "POST", Path: "/keep-awake", Write: true, Idempotent: true, Description: "Owners only. Keeps the computer Exhale runs on from sleeping: mode off, always, or hours (from and to, HH:MM in the household time zone; to earlier than from crosses midnight). keepDisplayOn keeps the screen on too. lidClosed keeps a laptop awake with its lid closed (on a Mac, setup_lid_closed first). onlyOnPower: only while plugged in. batteryFloor: on battery, stop at this charge (default 20). To keep it awake now for a while, use keep_awake_session. Can be reverted with undo.", }, (*Service).SetKeepAwake} OpKeepAwakeSession = Op[KeepAwakeSessionInput, KeepAwakeInfo]{Meta{ Name: "keep_awake_session", Title: "Keep this computer awake for a while", Tag: "Household", Method: "POST", Path: "/keep-awake/session", Write: true, Description: "Owners only. Keeps the computer Exhale runs on awake now, whatever the rule of set_keep_awake says: action start for minutes, until a time of day (HH:MM, household time zone) or until stopped; extend adds minutes; stop ends it. A session survives a restart of the computer with the time it had left. Can be reverted with undo.", }, (*Service).KeepAwakeSession} OpSetupLidClosed = Op[SetupLidClosedInput, KeepAwakeInfo]{Meta{ Name: "setup_lid_closed", Title: "Allow keeping a closed laptop awake", Tag: "Household", Method: "POST", Path: "/keep-awake/lid-setup", Write: true, Idempotent: true, Description: "Owners only, on a Mac: shows the system's administrator password dialog on the computer Exhale runs on, to allow it to turn lid sleep off (pmset disablesleep) without asking again. Someone must be at that computer. Elsewhere it only reports whether the lid can be held.", }, (*Service).SetupLidClosed} OpGlance = Op[GlanceInput, Glance]{Meta{ Name: "glance", Title: "A glance at what comes next", Tag: "Insights", Method: "GET", Path: "/glance", Description: "The next charges with their amounts already written for the member's language, what the next 30 days cost per currency, and whether this computer is kept awake. Made for small places: the menu bar icon, a widget.", }, (*Service).Glance} )
Tools for many subscriptions: bulk changes and tags (bulk.go).
var CardNoticeDays = 30
CardNoticeDays is how long before a card expires Exhale says so.
var DecisionDaysBefore = []int{7, 0}
DecisionDaysBefore is when to ask "keep or cancel?" before the last day to cancel a subscription with a notice period: a week ahead and on the day.
var ErrForbidden = errors.New("forbidden")
ErrForbidden is returned when the actor lacks the required scope.
var ErrNoDestination = errors.New("no linked chat")
ErrNoDestination is returned by a delivery when the member has nowhere to receive it: no chat or channel linked, or none of them working.
var ErrObsolete = errors.New("no longer needed")
ErrObsolete is returned by a delivery when the message has nothing left to say (the duplicate was resolved, the card renewed). The item is recorded as no longer needed instead of sent.
Functions ¶
func CollectAudits ¶
CollectAudits returns a context that records every change committed with it. Adapters that must offer undo for any use case (the in-app assistant) read the ids from the returned slice after the call, without each use case having to return them.
func DayRuleName ¶
DayRuleName and ShiftName are the API's words for a rule's options.
func DuplicateKey ¶
func DuplicateKey(subs []SubscriptionView) string
DuplicateKey identifies a group of subscriptions of the same service.
func NormalizeLogo ¶
NormalizeLogo decodes a PNG, JPEG or WebP picture and returns it as a PNG that fits in LogoSize×LogoSize, keeping its proportions.
func SnoozeDay ¶
func SnoozeDay(kind string, c ChargeView, today engine.Date, days int) (on engine.Date, clamped, ok bool)
SnoozeDay is the day a message snoozed for days comes back: never later than its limit. ok is false when it cannot come back after today.
func SnoozeLimit ¶
func SnoozeLimit(kind string, c ChargeView) (engine.Date, bool)
SnoozeLimit is the last day a message about a charge is still useful: a reminder until the charge date, a "keep or cancel?" until the last day to cancel. ok is false when there is no limit ("was it charged?").
Types ¶
type Actor ¶
type Actor struct {
Kind ActorKind
MemberID string
HouseholdID string
Label string // "Alex", "Claude Desktop (MCP)", "scheduler"
Scope Scope
}
Actor is the authenticated caller of a use case.
func MemberActor ¶
MemberActor returns an actor for a household member.
type AddDocumentInput ¶
type AddDocumentInput struct {
SubscriptionID string
ChargeID string
Kind string
Name string
Data []byte
DryRun bool
}
AddDocumentInput attaches a file. Uploads come over HTTP (multipart); the content is not part of the op catalog.
type AgentContext ¶
type AgentContext struct {
Markdown string `json:"markdown" jsonschema:"human-readable briefing of the household, its subscriptions and how Exhale works"`
}
AgentContext is the ready-made briefing an AI agent reads first (ADR 0018).
type AttentionList ¶
type AttentionList struct {
Unconfirmed []ChargeView `json:"unconfirmed" jsonschema:"charges whose due date passed and that are not marked paid, failed or skipped"`
ExpiringPaymentMethods []domain.PaymentMethod `json:"expiringPaymentMethods" jsonschema:"payment methods expiring within 45 days"`
Deadlines []ChargeView `` /* 128-byte string literal not displayed */
// Delivery reports reminders that could not be delivered lately, so a
// broken channel does not go unnoticed.
Delivery *DeliveryProblem `json:"delivery,omitempty" jsonschema:"set when reminders failed to be delivered in the last 7 days"`
}
AttentionList is what needs the user's attention.
type AuditEntry ¶
type AuditEntry struct {
ID string `json:"id"`
At string `json:"at"`
ActorKind string `json:"actorKind"`
ActorLabel string `json:"actor"`
Action string `json:"action"`
Entity string `json:"entity"`
EntityID string `json:"entityId"`
UndoneBy string `json:"undoneBy,omitempty"`
}
AuditEntry is one recorded change.
type AuditLog ¶
type AuditLog struct {
Entries []AuditEntry `json:"entries"`
}
AuditLog is the list of recent changes.
type AuditLogInput ¶
type AuditLogInput struct {
Limit int `json:"limit,omitempty" query:"limit" jsonschema:"default 50" maximum:"500"`
}
AuditLogInput limits the audit log.
type AuditRef ¶
type AuditRef struct {
ID string `json:"id"`
Action string `json:"action"`
Entity string `json:"entity"`
EntityID string `json:"entityId"`
}
AuditRef identifies a change recorded in the audit log.
type AuditReport ¶
type AuditReport struct {
Duplicates []DuplicateGroup `` /* 128-byte string literal not displayed */
PriceIncreases []PriceIncrease `json:"priceIncreases" jsonschema:"prices that went up in the last 12 months"`
TrialsEnding []ChargeView `json:"trialsEnding" jsonschema:"free trials that turn into a charge within 14 days"`
CancelDeadlines []ChargeView `json:"cancelDeadlines" jsonschema:"last days to cancel within 30 days that have no decision yet"`
Expiring []ChargeView `json:"expiring" jsonschema:"manual renewals that expire within 60 days"`
BigCharges []ChargeView `json:"bigCharges" jsonschema:"yearly or rarer charges due within 30 days"`
CardsExpiring []CardImpact `json:"cardsExpiring" jsonschema:"payment methods expiring within 45 days and the subscriptions that use them"`
Unconfirmed []ChargeView `json:"unconfirmed" jsonschema:"charges whose date passed without being confirmed"`
NotUsed []UnusedSubscription `json:"notUsed" jsonschema:"subscriptions the member said they no longer use"`
Findings int `json:"findings" jsonschema:"total number of findings"`
CheaperPlans []CheaperPlan `json:"cheaperPlans" jsonschema:"subscriptions where the member recorded another price (e.g. yearly) that costs less per year"`
}
AuditReport lists facts worth acting on. Everything here is computed, not guessed: an agent can turn it into advice.
type Balances ¶
type Balances struct {
Month string `json:"month"`
MainCurrency string `json:"mainCurrency"`
Members []MemberBalance `json:"members"`
Transfers []Transfer `json:"transfers" jsonschema:"the fewest payments that settle the month"`
Charges []SharedCharge `json:"charges" jsonschema:"the split charges behind the numbers (paid and still pending this month)"`
}
Balances says who owes whom for a month of shared subscriptions.
type BalancesInput ¶
type BalancesInput struct {
Month string `json:"month,omitempty" query:"month" jsonschema:"YYYY-MM, default the current month"`
}
BalancesInput selects the month to settle.
type Binding ¶
type Binding struct {
Meta
In, Out reflect.Type
Invoke func(s *Service, ctx context.Context, a Actor, raw json.RawMessage) (any, error)
}
Binding is the untyped view of an Op, for adapters that work with JSON.
type BootstrapInput ¶
type BootstrapInput struct {
HouseholdName string
OwnerName string
OwnerEmail string
Currency string
Timezone string
Locale string
}
BootstrapInput creates the household on first run.
type Breakdown ¶
type Breakdown struct {
Period string `json:"period" enum:"month,year"`
By string `json:"by" enum:"category,payment_method,subscription"`
Total money.Money `json:"total" doc:"in the main currency"`
Rows []BreakdownRow `json:"rows" doc:"largest first"`
Converted bool `json:"converted" doc:"other currencies were converted at the latest ECB reference rate"`
Unconverted []string `json:"unconverted" doc:"currencies without a known rate, left out"`
}
Breakdown is what active subscriptions cost per month or per year, in the main currency, grouped by category, payment method or subscription. The assistant shows it as a screen (ADR 0019 §4); it is not an op of its own: get_summary answers the same question for agents.
type BreakdownRow ¶
type BreakdownRow struct {
Label string `json:"label"`
SubscriptionID string `json:"subscriptionId,omitempty" doc:"when the breakdown is by subscription"`
Amount money.Money `json:"amount"`
Count int `json:"count"`
}
BreakdownRow is one group. Label is empty for subscriptions without a category or a payment method: the screen names that in its language.
type BulkItem ¶
type BulkItem struct {
ID string `json:"id"`
Name string `json:"name"`
Changes []FieldChange `json:"changes" jsonschema:"empty: nothing changes for this one"`
}
BulkItem is what happens (or would happen) to one subscription.
type BulkResult ¶
type BulkResult struct {
Items []BulkItem `json:"items"`
Changed int `json:"changed" jsonschema:"subscriptions that change"`
AuditIDs []string `json:"auditIds" jsonschema:"pass them all to undo (auditIds) to revert the whole change; empty in a dry run"`
DryRun bool `json:"dryRun,omitempty"`
}
BulkResult reports a change to several subscriptions.
type BulkUpdateInput ¶
type BulkUpdateInput struct {
IDs []string `json:"ids" jsonschema:"subscription ids (sub_…), at most 500" minItems:"1" maxItems:"500"`
Action string `` /* 234-byte string literal not displayed */
CategoryID *string `json:"categoryId,omitempty" jsonschema:"with update: the new category; empty string removes it"`
PaymentMethodID *string `json:"paymentMethodId,omitempty" jsonschema:"with update: the new payment method; empty string removes it"`
AddTags []string `json:"addTags,omitempty" jsonschema:"with update: tags to add to each"`
RemoveTags []string `json:"removeTags,omitempty" jsonschema:"with update: tags to remove from each"`
DryRun bool `json:"dryRun,omitempty" jsonschema:"preview what changes for each subscription; nothing is saved"`
}
BulkUpdateInput changes many subscriptions in one step.
type CalendarEvent ¶
type CalendarEvent struct {
Date engine.Date `json:"date"`
Kind string `` /* 158-byte string literal not displayed */
Title string `json:"title"`
SubscriptionID string `json:"subscriptionId"`
Subscription string `json:"subscription"`
Amount money.Money `json:"amount"`
ChargeID string `json:"chargeId,omitempty" jsonschema:"empty for dates projected from the schedule beyond the next few months"`
Status string `json:"status,omitempty" jsonschema:"charge status; projected dates are pending"`
Projected bool `json:"projected" jsonschema:"computed from the schedule; the charge does not exist yet"`
}
CalendarEvent is one dated thing worth seeing in a calendar.
type CalendarInput ¶
type CalendarInput struct {
From string `json:"from,omitempty" query:"from" jsonschema:"first day, YYYY-MM-DD (default today)"`
To string `json:"to,omitempty" query:"to" jsonschema:"last day, YYYY-MM-DD (default one year after from; at most three years)"`
}
CalendarInput selects the dates to show.
type CalendarView ¶
type CalendarView struct {
From engine.Date `json:"from"`
To engine.Date `json:"to"`
Events []CalendarEvent `json:"events"`
}
CalendarView lists the events of a period.
type CardImpact ¶
type CardImpact struct {
PaymentMethod domain.PaymentMethod `json:"paymentMethod"`
Subscriptions []SubscriptionRef `json:"subscriptions" jsonschema:"active subscriptions that will fail when it expires"`
}
CardImpact is a payment method about to expire and what depends on it.
type CardNotice ¶
type CardNotice struct {
PaymentMethod domain.PaymentMethod
DaysLeft int
Subscriptions []SubscriptionView
// Others are the payment methods they can move to.
Others []domain.PaymentMethod
}
CardNotice is a payment method about to expire with the subscriptions that use it, as the recipient sees them.
type CategoryOut ¶
CategoryOut wraps a created category.
type CategorySpend ¶
type CategorySpend struct {
Category string `json:"category"`
Monthly money.Money `json:"monthly" jsonschema:"in the main currency"`
Count int `json:"count"`
}
CategorySpend is the monthly spend of one category.
type ChangeChargeAmountInput ¶
type ChangeChargeAmountInput struct {
ChargeID string `json:"chargeId"`
Amount string `json:"amount" jsonschema:"new amount as a decimal string, in the subscription currency"`
Scope string `` /* 164-byte string literal not displayed */
DryRun bool `json:"dryRun,omitempty"`
}
ChangeChargeAmountInput changes what a charge costs.
type ChangeChargeDateInput ¶
type ChangeChargeDateInput struct {
ChargeID string `json:"chargeId" jsonschema:"charge id (chg_…)"`
NewDate string `json:"newDate" jsonschema:"new due date, YYYY-MM-DD"`
Scope string `` /* 149-byte string literal not displayed */
DryRun bool `json:"dryRun,omitempty"`
}
ChangeChargeDateInput moves a charge.
type ChargeList ¶
type ChargeList struct {
From engine.Date `json:"from"`
To engine.Date `json:"to"`
Charges []ChargeView `json:"charges"`
}
ChargeList is a list of charges.
type ChargeMatch ¶
type ChargeMatch struct {
Charge ChargeView `json:"charge"`
Transaction StatementLine `json:"transaction"`
Paid money.Money `json:"paid" jsonschema:"what the transaction took"`
Action string `` /* 196-byte string literal not displayed */
// PriceChange is set when the bank took another amount in the same
// currency: ask whether it is the new price.
PriceChange *PriceChangeHint `` /* 194-byte string literal not displayed */
}
ChargeMatch is an expected charge and the transaction that paid it.
type ChargeResult ¶
type ChargeResult struct {
Charge ChargeView `json:"charge"`
Subscription SubscriptionView `json:"subscription"`
AuditID string `json:"auditId,omitempty" jsonschema:"pass to undo to revert this change"`
DryRun bool `json:"dryRun,omitempty"`
// PriceChange is set when a charge was paid at a different amount than
// expected, in the same currency: probably a new price.
PriceChange *PriceChangeHint `` /* 175-byte string literal not displayed */
}
ChargeResult is returned by every charge edit.
type ChargeView ¶
type ChargeView struct {
domain.Charge
SubscriptionName string `json:"subscriptionName"`
PaymentMethod string `json:"paymentMethod,omitempty"`
Delayed bool `json:"delayed" jsonschema:"true when the charge was moved from its scheduled date"`
DaysUntil int `json:"daysUntil" jsonschema:"days from today to the due date (negative if overdue)"`
// Kind tells what this date means to the member.
Kind string `` /* 190-byte string literal not displayed */
CancelBy engine.Date `json:"cancelBy,omitzero" jsonschema:"last day to cancel before this charge, when the subscription has a notice period"`
CancelPassed bool `json:"cancelPassed" jsonschema:"the last day to cancel before this charge has passed: it will renew"`
Logo string `json:"logo,omitempty" jsonschema:"path of the subscription's uploaded logo, if any"`
}
ChargeView is a charge with its subscription context.
type CheaperPlan ¶
type CheaperPlan struct {
Subscription SubscriptionRef `json:"subscription"`
Other domain.AltPrice `json:"other" jsonschema:"the other price and the period it covers"`
PlanSwitch
}
CheaperPlan is another price the member recorded that would cost less.
type CreateCategoryInput ¶
type CreateCategoryInput struct {
Name string `json:"name"`
Color string `json:"color,omitempty" jsonschema:"hex color, e.g. #7c6cff"`
DryRun bool `json:"dryRun,omitempty"`
}
CreateCategoryInput creates a category.
type CreateSubscriptionInput ¶
type CreateSubscriptionInput struct {
Name string `json:"name" jsonschema:"service name, e.g. Netflix Premium" minLength:"1" maxLength:"120"`
Price string `json:"price" jsonschema:"price per charge as a decimal string, e.g. 12.99"`
Currency string `json:"currency" jsonschema:"ISO 4217 code, e.g. EUR"`
Interval int `json:"interval,omitempty" jsonschema:"number of units between charges, default 1" minimum:"1"`
Unit string `json:"unit,omitempty" jsonschema:"day, week, month (default) or year" enum:"day,week,month,year"`
FirstCharge string `` /* 126-byte string literal not displayed */
DayRule string `` /* 470-byte string literal not displayed */
WeekendShift string `` /* 343-byte string literal not displayed */
CategoryID string `json:"categoryId,omitempty"`
PaymentMethodID string `json:"paymentMethodId,omitempty"`
PayerID string `json:"payerId,omitempty" jsonschema:"member who pays; defaults to the caller"`
Private bool `json:"private,omitempty" jsonschema:"only visible to its creator and payer"`
URL string `json:"url,omitempty"`
Notes string `json:"notes,omitempty"`
AutoConfirm bool `json:"autoConfirm,omitempty" jsonschema:"mark each charge paid automatically on its due date"`
ReminderDays []int `json:"reminderDays,omitempty" jsonschema:"days before each charge to remind, default [3, 0]"`
EndDate string `json:"endDate,omitempty" jsonschema:"last possible charge date, YYYY-MM-DD"`
Status string `` /* 210-byte string literal not displayed */
TermsInput
ExtrasInput
DryRun bool `json:"dryRun,omitempty" jsonschema:"preview the result without saving"`
}
CreateSubscriptionInput describes a new subscription.
type DecideRenewalInput ¶
type DecideRenewalInput struct {
ChargeID string `json:"chargeId"`
Decision string `json:"decision" jsonschema:"keep: stop asking before the last day to cancel; undecided: ask again" enum:"keep,undecided"`
DryRun bool `json:"dryRun,omitempty"`
}
DecideRenewalInput records what the member decided about a renewal.
type DeleteCategoryInput ¶
type DeleteCategoryInput struct {
ID string `json:"id" jsonschema:"category id (cat_…)"`
DryRun bool `json:"dryRun,omitempty"`
}
DeleteCategoryInput removes a category.
type DeleteDocumentInput ¶
type DeleteDocumentInput struct {
ID string `json:"id" jsonschema:"document id (doc_…)"`
DryRun bool `json:"dryRun,omitempty"`
}
DeleteDocumentInput names the document.
type DeletePaymentMethodInput ¶
type DeletePaymentMethodInput struct {
ID string `json:"id" jsonschema:"payment method to delete (pm_…)"`
MoveTo string `json:"moveTo,omitempty" jsonschema:"payment method that takes over its subscriptions; empty leaves them without one"`
DryRun bool `json:"dryRun,omitempty"`
}
DeletePaymentMethodInput removes a payment method.
type DeleteResult ¶
type DeleteResult struct {
Deleted string `json:"deleted"`
AuditID string `json:"auditId" jsonschema:"pass to undo to restore"`
}
DeleteResult confirms a deletion.
type DeleteSubscriptionInput ¶
DeleteSubscriptionInput permanently deletes a subscription and its charges.
type DeleteTagInput ¶
type DeleteTagInput struct {
Tag string `json:"tag" jsonschema:"the tag to remove (case does not matter)"`
DryRun bool `json:"dryRun,omitempty"`
}
DeleteTagInput removes a tag from every subscription.
type DeliveryProblem ¶
type DeliveryProblem struct {
Failed int `json:"failed" jsonschema:"reminders not delivered in the last 7 days"`
NoChannel int `json:"noChannel" jsonschema:"of those, how many had nowhere to go (no chat or channel linked)"`
LastError string `json:"lastError,omitempty" jsonschema:"most recent delivery error"`
}
DeliveryProblem says reminders are not arriving.
type DismissDuplicateInput ¶
type DismissDuplicateInput struct {
Key string `json:"key" jsonschema:"key of the duplicate group, from audit"`
DryRun bool `json:"dryRun,omitempty"`
}
DismissDuplicateInput says a duplicate is fine.
type DismissResult ¶
DismissResult confirms a finding was set aside.
type Document ¶
type Document struct {
ID string `json:"id"`
SubscriptionID string `json:"subscriptionId"`
ChargeID string `json:"chargeId,omitempty" jsonschema:"the charge this invoice or receipt belongs to, if any"`
Kind string `` /* 144-byte string literal not displayed */
Name string `json:"name" jsonschema:"file name as uploaded"`
MediaType string `json:"mediaType" jsonschema:"application/pdf, image/png, image/jpeg or image/webp"`
Size int64 `json:"size" jsonschema:"bytes"`
SHA256 string `json:"sha256"`
CreatedAt time.Time `json:"createdAt"`
CreatedBy string `json:"createdBy" jsonschema:"who attached it"`
// URL is where the content is downloaded, with the caller's credentials.
URL string `json:"url" jsonschema:"path to download the file, under this Exhale's address"`
// Path is the file inside a full export archive; empty elsewhere.
Path string `json:"path,omitempty"`
}
Document is a file attached to a subscription, without its content.
type DocumentList ¶
type DocumentList struct {
Documents []Document `json:"documents"`
}
DocumentList is the documents of one subscription, newest first.
type DocumentResult ¶
type DocumentResult struct {
Document Document `json:"document"`
AuditID string `json:"auditId,omitempty" jsonschema:"pass to undo to revert"`
}
DocumentResult is a document after a change, with the audit id for undo.
type DuplicateGroup ¶
type DuplicateGroup struct {
Key string `json:"key" jsonschema:"stable id of the group; pass it to dismiss_duplicate when paying twice is on purpose"`
Service string `json:"service"`
Subscriptions []SubscriptionRef `json:"subscriptions"`
}
DuplicateGroup is the same service paid more than once.
type DuplicateNotice ¶
type DuplicateNotice struct {
Group DuplicateGroup
// Subscriptions in the same order as Group.Subscriptions.
Subscriptions []SubscriptionView
}
DuplicateNotice is the group a duplicate message is about, as the recipient sees it.
type ExportData ¶
type ExportData struct {
Format string `json:"format" jsonschema:"always exhale/1"`
ExportedAt time.Time `json:"exportedAt"`
Household domain.Household `json:"household"`
Members []domain.Member `json:"members"`
Categories []domain.Category `json:"categories"`
PaymentMethods []domain.PaymentMethod `json:"paymentMethods"`
Subscriptions []ExportedSubscription `json:"subscriptions"`
}
ExportData is everything the household keeps in Exhale, in one document.
type ExportedSubscription ¶
type ExportedSubscription struct {
SubscriptionView
Charges []domain.Charge `json:"charges"`
// Files are its uploaded logo and documents, without their content:
// the full export archive and the backups carry the content (ADR 0027).
Files []Document `json:"files"`
}
ExportedSubscription is a subscription with every charge it has had.
type ExtrasInput ¶
type ExtrasInput struct {
Tags []string `json:"tags,omitempty" jsonschema:"free labels, many per subscription, e.g. work or family. Totals stay by category"`
AltPrice string `` /* 225-byte string literal not displayed */
AltUnit string `json:"altUnit,omitempty" jsonschema:"the period altPrice covers: month or year (default: year)" enum:"month,year"`
}
ExtrasInput are the tags and the alternative price of a new subscription.
type ExtrasUpdate ¶
type ExtrasUpdate struct {
Tags []string `json:"tags,omitempty" jsonschema:"replaces every tag; an empty list removes them all"`
AltPrice *string `` /* 164-byte string literal not displayed */
AltUnit *string `json:"altUnit,omitempty" jsonschema:"the period altPrice covers: month or year" enum:"month,year"`
}
ExtrasUpdate changes the tags or the alternative price.
type FieldChange ¶
type FieldChange struct {
Field string `json:"field" jsonschema:"category, paymentMethod, tags, status or deleted" enum:"category,paymentMethod,tags,status,deleted"`
From string `json:"from"`
To string `json:"to"`
}
FieldChange is one field that changes on one subscription.
type GetSubscriptionInput ¶
type GetSubscriptionInput struct {
ID string `json:"id" path:"id" jsonschema:"subscription id (sub_…)"`
}
GetSubscriptionInput identifies a subscription.
type Glance ¶ added in v0.4.0
type Glance struct {
Language string `json:"language" jsonschema:"the member's language, for the client's own words"`
Today engine.Date `json:"today"`
Charges []GlanceCharge `json:"charges" jsonschema:"the next open charges of the next 30 days, soonest first"`
Totals []string `json:"totals" jsonschema:"what the next 30 days cost, one written amount per currency"`
Awake KeepAwakeInfo `json:"keepAwake"`
Overdue int `json:"overdue" jsonschema:"charges whose date passed without being confirmed"`
}
Glance is what comes next.
type GlanceCharge ¶ added in v0.4.0
type GlanceCharge struct {
SubscriptionID string `json:"subscriptionId"`
Name string `json:"name"`
Amount string `json:"amount" jsonschema:"written for the member's language, e.g. 12,99 €"`
Date engine.Date `json:"date"`
DaysUntil int `json:"daysUntil"`
Kind string `json:"kind" enum:"renewal,trial,expiry"`
}
GlanceCharge is one charge, written out.
type GlanceInput ¶ added in v0.4.0
type GlanceInput struct {
Limit int `query:"limit" json:"limit,omitempty" minimum:"0" maximum:"20" jsonschema:"how many charges, default 5"`
}
GlanceInput says how much to show.
type HouseholdInfo ¶
type HouseholdInfo struct {
Household domain.Household `json:"household"`
Members []domain.Member `json:"members"`
Categories []domain.Category `json:"categories"`
PaymentMethods []domain.PaymentMethod `json:"paymentMethods"`
Today engine.Date `json:"today" jsonschema:"today's date in the household time zone"`
Tags []TagCount `json:"tags" jsonschema:"every tag in use, with how many subscriptions carry it"`
}
HouseholdInfo is the household with its members, categories and payment methods.
func (HouseholdInfo) LanguageOf ¶
func (i HouseholdInfo) LanguageOf(memberID string) string
LanguageOf returns the language to speak to a member: their own, else the household's.
type ImportInput ¶
type ImportInput struct {
Items []ImportItem `json:"items"`
AllowDuplicates bool `` /* 131-byte string literal not displayed */
DryRun bool `json:"dryRun,omitempty" jsonschema:"preview: nothing is saved. Show the result to the user, then call again without it"`
}
ImportInput brings in several subscriptions at once.
type ImportItem ¶
type ImportItem struct {
Name string `json:"name"`
Price string `json:"price" jsonschema:"decimal string, e.g. 12.99"`
Currency string `json:"currency,omitempty" jsonschema:"ISO 4217 code; default: the household's display currency"`
Interval int `json:"interval,omitempty" jsonschema:"units between charges, default 1"`
Unit string `json:"unit,omitempty" jsonschema:"day, week, month (default) or year" enum:"day,week,month,year"`
NextCharge string `json:"nextCharge,omitempty" jsonschema:"next (or first) charge, YYYY-MM-DD. For an ended subscription: when it started"`
DayRule string `` /* 240-byte string literal not displayed */
WeekendShift string `` /* 202-byte string literal not displayed */
StartedOn string `json:"startedOn,omitempty" jsonschema:"when the member first subscribed, YYYY-MM-DD"`
EndDate string `json:"endDate,omitempty" jsonschema:"last day, for subscriptions that ended, YYYY-MM-DD"`
Status string `json:"status,omitempty" jsonschema:"active (default), paused or cancelled" enum:"active,paused,cancelled"`
Category string `json:"category,omitempty" jsonschema:"category name; created if it does not exist"`
PaymentMethod string `json:"paymentMethod,omitempty" jsonschema:"payment method name; created if it does not exist"`
Payer string `json:"payer,omitempty" jsonschema:"member name; default: the caller"`
URL string `json:"url,omitempty"`
Notes string `json:"notes,omitempty"`
CancelURL string `json:"cancelUrl,omitempty"`
Account string `json:"account,omitempty"`
Renewal string `json:"renewal,omitempty" jsonschema:"auto (default) or manual" enum:"auto,manual"`
NoticeDays int `json:"noticeDays,omitempty"`
TrialEndsOn string `json:"trialEndsOn,omitempty"`
ReminderDays []int `json:"reminderDays,omitempty" jsonschema:"days before each charge to remind; default [3, 0]"`
AutoConfirm bool `json:"autoConfirm,omitempty"`
Tags []string `json:"tags,omitempty" jsonschema:"free labels"`
AltPrice string `json:"altPrice,omitempty" jsonschema:"another price the provider offers, decimal string, e.g. 99.00"`
AltUnit string `json:"altUnit,omitempty" jsonschema:"the period altPrice covers: month or year" enum:"month,year"`
// Remark is a note from whoever prepared the item (a file parser or an
// agent) for the person reviewing it. It is not saved.
Remark string `json:"remark,omitempty" jsonschema:"note for the person reviewing the import; not saved"`
}
ImportItem is one subscription to bring in. Categories, payment methods and payers are given by name, so a spreadsheet, another app's export or an AI agent reading a bank statement can fill it without knowing ids.
type ImportOutcome ¶
type ImportOutcome struct {
Name string `json:"name"`
Action string `json:"action" jsonschema:"create, duplicate (skipped) or error (skipped)" enum:"create,duplicate,error"`
ID string `json:"id,omitempty" jsonschema:"new subscription id; not saved in a dry run"`
Message string `json:"message,omitempty"`
}
ImportOutcome is what happened (or would happen) to one item.
type ImportResult ¶
type ImportResult struct {
Created int `json:"created"`
Duplicates int `json:"duplicates"`
Errors int `json:"errors"`
Items []ImportOutcome `json:"items"`
NewCategories []string `json:"newCategories" jsonschema:"categories created along the way"`
NewPaymentMethods []string `json:"newPaymentMethods" jsonschema:"payment methods created along the way"`
DryRun bool `json:"dryRun,omitempty"`
}
ImportResult summarises an import.
type KeepAwake ¶
type KeepAwake struct {
// Lid keeps the computer awake with its lid closed; nil: not here.
Lid awake.Lid
// Power reads the power source; nil: unknown (never paused).
Power func() awake.Power
// MaxSleep bounds every wait, so a change made by another process
// (exhale mcp) or a clock jump is noticed.
MaxSleep time.Duration
// RetryAfter is how long a refused hold waits before it is tried again
// with the same setting.
RetryAfter time.Duration
// contains filtered or unexported fields
}
KeepAwake applies the keep-awake setting with a Keeper: when the server starts, on every change and at the edges of the hours. It is a supervised service of exhale serve.
func NewKeepAwake ¶
NewKeepAwake returns the controller and attaches it to svc, whose keep-awake operations then report its status.
func (*KeepAwake) Apply ¶
Apply holds or releases the machine as the setting asks now, and returns when that next changes by itself (zero: not by itself).
func (*KeepAwake) Release ¶
func (k *KeepAwake) Release()
Release lets the machine sleep again, lid included.
func (*KeepAwake) Serve ¶
Serve applies the setting until ctx is cancelled, then lets the machine sleep again. It first undoes a lid hold a crash left behind and resumes a session the last stop paused; when it stops, it pauses the session.
func (*KeepAwake) SetupLid ¶ added in v0.4.0
SetupLid asks the computer for the permission to hold the lid.
func (*KeepAwake) Status ¶
func (k *KeepAwake) Status() KeepAwakeStatus
Status is the state after the last Apply.
type KeepAwakeInfo ¶
type KeepAwakeInfo struct {
KeepAwakeSetting
Timezone string `json:"timezone" jsonschema:"the time zone of from and to (the household's)"`
Status KeepAwakeStatus `json:"status"`
}
KeepAwakeInfo is the setting with its live status.
type KeepAwakePower ¶ added in v0.4.0
type KeepAwakePower struct {
HasBattery bool `json:"hasBattery" jsonschema:"false on a desktop"`
OnBattery bool `json:"onBattery"`
Percent int `json:"percent" jsonschema:"battery charge, 0 to 100"`
}
KeepAwakePower is the computer's power source.
type KeepAwakeSession ¶ added in v0.4.0
type KeepAwakeSession struct {
Active bool `json:"active"`
// Until is when it ends (RFC 3339); empty: when it is stopped.
Until string `json:"until,omitempty" jsonschema:"when the session ends, RFC 3339; empty when it lasts until stopped"`
// Left is the time it had left when the server stopped, in seconds:
// a computer that restarts gets it back in full.
Left int `json:"left,omitempty" jsonschema:"seconds the session had left when Exhale stopped; it resumes with them"`
}
KeepAwakeSession keeps the computer awake now, whatever the rule says.
type KeepAwakeSessionInput ¶ added in v0.4.0
type KeepAwakeSessionInput struct {
Action string `` /* 259-byte string literal not displayed */
Minutes int `json:"minutes,omitempty" minimum:"0" maximum:"10080" jsonschema:"how long, in minutes (start, extend)"`
Until string `` /* 129-byte string literal not displayed */
DryRun bool `json:"dryRun,omitempty"`
}
KeepAwakeSessionInput starts, extends or stops a session that keeps the computer awake now, whatever the rule says.
type KeepAwakeSetting ¶
type KeepAwakeSetting struct {
Mode string `` /* 153-byte string literal not displayed */
From string `json:"from" jsonschema:"start of the hours, HH:MM in the household time zone"`
To string `json:"to" jsonschema:"end of the hours, HH:MM in the household time zone; earlier than from for hours that cross midnight"`
// The system is kept awake; the display may still sleep unless this is set.
KeepDisplayOn bool `json:"keepDisplayOn" jsonschema:"also keep the display on (default false: only the system stays awake)"`
LidClosed bool `json:"lidClosed" jsonschema:"keep a laptop awake with its lid closed too (on a Mac, needs setup_lid_closed once)"`
OnlyOnPower bool `json:"onlyOnPower" jsonschema:"only while plugged in: a laptop on battery sleeps as usual"`
BatteryFloor int `` /* 143-byte string literal not displayed */
// Session keeps it awake on top of the rule, for a while or until stopped.
Session KeepAwakeSession `json:"session,omitzero"`
}
KeepAwakeSetting is when Exhale keeps the computer it runs on awake.
type KeepAwakeStatus ¶
type KeepAwakeStatus struct {
State string `` /* 307-byte string literal not displayed */
// Why it is paused or unavailable.
Reason string `` /* 287-byte string literal not displayed */
Error string `json:"error,omitempty" jsonschema:"what the system said, in its words"`
// Until is when keeping awake ends by itself: the session's end, or the
// end of the hours.
Until string `` /* 137-byte string literal not displayed */
// Session is true when a session, not the rule, keeps it awake now.
Session bool `json:"session" jsonschema:"a session started by hand keeps it awake now"`
// Lid is whether a closed lid is kept awake too.
Lid string `` /* 272-byte string literal not displayed */
LidError string `json:"lidError,omitempty"`
// Power is the computer's power source, when the system says.
Power KeepAwakePower `json:"power,omitzero"`
}
KeepAwakeStatus says whether the computer is kept awake right now.
type ListChargesInput ¶
type ListChargesInput struct {
From string `json:"from,omitempty" query:"from" jsonschema:"first due date, YYYY-MM-DD (default today)"`
To string `json:"to,omitempty" query:"to" jsonschema:"last due date, YYYY-MM-DD (default from + 30 days)"`
}
ListChargesInput selects charges by due date.
type ListDocumentsInput ¶
type ListDocumentsInput struct {
ID string `json:"id" path:"id" jsonschema:"subscription id (sub_…)"`
}
ListDocumentsInput names the subscription.
type ListSubscriptionsInput ¶
type ListSubscriptionsInput struct {
Status string `json:"status,omitempty" query:"status" jsonschema:"active, paused or cancelled; empty for all"`
Query string `json:"query,omitempty" query:"query" jsonschema:"case-insensitive search in the name, notes, account, category and tags"`
Tag string `json:"tag,omitempty" query:"tag" jsonschema:"only subscriptions with this tag (case-insensitive)"`
}
ListSubscriptionsInput filters subscriptions.
type LogoResult ¶
type LogoResult struct {
SubscriptionID string `json:"subscriptionId"`
Logo string `json:"logo,omitempty" jsonschema:"path of the logo now shown; empty: the bundled brand mark or the monogram"`
AuditID string `json:"auditId,omitempty" jsonschema:"pass to undo to revert"`
}
LogoResult is the logo after a change.
type MatchStatementInput ¶
type MatchStatementInput struct {
Transactions []StatementLine `json:"transactions" jsonschema:"the payments of the statement"`
From string `json:"from,omitempty" jsonschema:"first day the statement covers, YYYY-MM-DD (default: the first transaction)"`
To string `json:"to,omitempty" jsonschema:"last day the statement covers, YYYY-MM-DD (default: the last transaction)"`
}
MatchStatementInput is a statement to compare with the expected charges.
type MemberBalance ¶
type MemberBalance struct {
MemberID string `json:"memberId"`
Name string `json:"name"`
Paid money.Money `json:"paid" jsonschema:"what this member's payment methods paid for shared subscriptions"`
Net money.Money `json:"net" jsonschema:"paid minus share: positive is owed money, negative owes money"`
}
MemberBalance is one member's position for the month.
type Meta ¶
type Meta struct {
Name string // tool name, snake_case
Title string
Description string
Write bool // changes data; needs write scope
Destructive bool // deletes or cancels; agents need confirmation
Idempotent bool // repeating the call has no further effect
Method string
Path string // REST path under /api/v1
Tag string
}
Meta describes a use case for every adapter that exposes it: the REST API (huma), the MCP server and the in-app assistant (ADR 0018).
type MethodSpend ¶
type MethodSpend struct {
PaymentMethod string `json:"paymentMethod"`
Monthly money.Money `json:"monthly" jsonschema:"in the main currency"`
Count int `json:"count"`
}
MethodSpend is the monthly spend carried by one payment method.
type MonthSpend ¶
type MonthSpend struct {
Month string `json:"month" jsonschema:"YYYY-MM"`
Paid money.Money `json:"paid" jsonschema:"charges marked paid that month, in the display currency"`
Expected money.Money `` /* 127-byte string literal not displayed */
Count int `json:"count" jsonschema:"paid plus expected charges"`
}
MonthSpend is the money of one calendar month.
type Op ¶
type Op[In, Out any] struct { Meta Run func(s *Service, ctx context.Context, a Actor, in In) (Out, error) }
Op is a typed use case.
type OutboxItem ¶
type OutboxItem struct {
ID string
Kind string
ChargeID string
MemberID string
DueAt time.Time
Attempts int
// Ref is what an item that is not about a charge is about: a payment
// method (KindCardExpiry) or a duplicate group (KindDuplicate).
Ref string
// Personal is set for an item one member asked for (a snooze): it goes
// to them only, never to a shared team channel.
Personal bool
}
OutboxItem is something the scheduler must deliver now.
type PaidTotals ¶
type PaidTotals struct {
Count int `json:"count" jsonschema:"charges marked paid"`
Total money.Money `json:"total" jsonschema:"sum of paid charges, in the subscription currency"`
Since engine.Date `json:"since,omitzero" jsonschema:"date of the first paid charge"`
}
PaidTotals is what a subscription has really cost so far.
type PaydayOutlook ¶
type PaydayOutlook struct {
Payday engine.Date `json:"payday" jsonschema:"next payday"`
Due money.Money `json:"due" jsonschema:"total of the charges due before it, in the display currency"`
Charges []ChargeView `json:"charges"`
}
PaydayOutlook is what must be paid before the next payday.
type PaymentMethodOut ¶
type PaymentMethodOut struct {
PaymentMethod domain.PaymentMethod `json:"paymentMethod"`
}
PaymentMethodOut wraps a saved payment method.
type PlanSwitch ¶
type PlanSwitch struct {
Current money.Money `json:"currentYearly" jsonschema:"what the current plan costs per year"`
Other money.Money `json:"otherYearly" jsonschema:"what the other price costs per year"`
// Saving is positive when switching saves money, negative when the
// current plan is already the cheaper one.
Saving money.Money `` /* 155-byte string literal not displayed */
}
PlanSwitch compares the current plan with the other price the member recorded.
type PreviewScheduleInput ¶
type PreviewScheduleInput struct {
Interval int `json:"interval" minimum:"1"`
Unit string `json:"unit" enum:"day,week,month,year"`
FirstCharge string `json:"firstCharge" jsonschema:"YYYY-MM-DD"`
DayRule string `` /* 470-byte string literal not displayed */
WeekendShift string `` /* 343-byte string literal not displayed */
Count int `json:"count,omitempty" jsonschema:"how many dates, default 12" maximum:"60"`
}
PreviewScheduleInput describes a billing rule to preview.
type PriceChangeHint ¶
type PriceChangeHint struct {
Expected money.Money `json:"expected"`
Charged money.Money `json:"charged"`
Change string `json:"changePercent"`
YearlyDelta money.Money `json:"yearlyDelta" jsonschema:"difference per year if this is the new price, in the subscription currency"`
}
PriceChangeHint says a charge was paid at another amount.
type PriceIncrease ¶
type PriceIncrease struct {
Subscription SubscriptionRef `json:"subscription"`
From money.Money `json:"from"`
To money.Money `json:"to"`
Since engine.Date `json:"since"`
Change string `json:"changePercent"`
YearlyDelta money.Money `json:"yearlyDelta" jsonschema:"extra cost per year at the new price, in the subscription currency"`
}
PriceIncrease is a price that went up in the last year.
type PricePoint ¶
type PricePoint struct {
From engine.Date `json:"from" jsonschema:"first charge at this price"`
Price money.Money `json:"price"`
Change string `json:"changePercent,omitempty" jsonschema:"change against the previous price, e.g. +14.3"`
Scheduled bool `json:"scheduled,omitempty" jsonschema:"a change announced in advance that has not arrived yet"`
}
PricePoint is one price a subscription has had (or will have).
type ReassignPaymentMethodInput ¶
type ReassignPaymentMethodInput struct {
FromID string `json:"fromId" jsonschema:"payment method the subscriptions use now (pm_…)"`
ToID string `json:"toId,omitempty" jsonschema:"payment method to move them to; empty leaves them without one"`
DryRun bool `json:"dryRun,omitempty"`
}
ReassignPaymentMethodInput moves every subscription from one payment method to another.
type ReassignResult ¶
type ReassignResult struct {
Moved int `json:"moved" jsonschema:"subscriptions that changed payment method"`
Deleted string `json:"deleted,omitempty" jsonschema:"payment method that was deleted, if any"`
}
ReassignResult says how many subscriptions moved.
type ReminderEntry ¶
type ReminderEntry struct {
ID string `json:"id"`
Kind string `json:"kind" jsonschema:"reminder (before a charge), decision (before the last day to cancel) or confirm (was it charged?)"`
Status string `json:"status" jsonschema:"pending, sent, failed or cancelled" enum:"pending,sent,failed,cancelled"`
DueAt time.Time `json:"dueAt" jsonschema:"when it is or was meant to go out"`
SentAt *time.Time `json:"sentAt,omitempty"`
Attempts int `json:"attempts"`
Error string `json:"error,omitempty" jsonschema:"last delivery error"`
SubscriptionID string `json:"subscriptionId"`
Subscription string `json:"subscription"`
ChargeDate engine.Date `json:"chargeDate"`
To string `json:"to,omitempty" jsonschema:"member it is for"`
Explanation string `json:"explanation" jsonschema:"what happened, in plain words"`
}
ReminderEntry is one reminder Exhale planned, sent or failed to send.
type ReminderLog ¶
type ReminderLog struct {
Entries []ReminderEntry `json:"entries"`
}
ReminderLog is the answer to "why didn't I get that reminder?".
type ReminderLogInput ¶
type ReminderLogInput struct {
Limit int `json:"limit,omitempty" query:"limit" jsonschema:"how many entries, newest first (default 100)" maximum:"500"`
}
ReminderLogInput selects how much of the log to read.
type RemoveLogoInput ¶
type RemoveLogoInput struct {
ID string `json:"id" jsonschema:"subscription id (sub_…)"`
DryRun bool `json:"dryRun,omitempty"`
}
RemoveLogoInput names the subscription.
type RenameTagInput ¶
type RenameTagInput struct {
Tag string `json:"tag" jsonschema:"the tag to rename (case does not matter)"`
To string `json:"to" jsonschema:"the new name; if a subscription already has it, the two merge"`
DryRun bool `json:"dryRun,omitempty"`
}
RenameTagInput renames a tag on every subscription that has it.
type SavePaymentMethodInput ¶
type SavePaymentMethodInput struct {
ID string `json:"id,omitempty" jsonschema:"existing payment method id (pm_…) to update; empty to create"`
Name string `json:"name" jsonschema:"e.g. Revolut, Visa, PayPal"`
Kind string `json:"kind" enum:"card,bank,paypal,other"`
Last4 string `json:"last4,omitempty" jsonschema:"last four digits" maxLength:"4"`
ExpiresOn string `json:"expiresOn,omitempty" jsonschema:"last valid day, YYYY-MM-DD (for cards: last day of the expiry month)"`
OwnerID string `json:"ownerId,omitempty"`
DryRun bool `json:"dryRun,omitempty"`
}
SavePaymentMethodInput creates (no id) or updates a payment method.
type SchedulePreview ¶
SchedulePreview lists the computed charge dates.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service implements the use cases.
func (*Service) ActorForMember ¶
ActorForMember returns the actor of a member (used by channel adapters).
func (*Service) AddDocument ¶
func (s *Service) AddDocument(ctx context.Context, actor Actor, in AddDocumentInput) (DocumentResult, error)
AddDocument attaches a PDF or a picture to a subscription, or to one of its charges (an invoice or a receipt).
func (*Service) Audit ¶
Audit reports duplicates, price increases, trials about to charge, cancellation deadlines, things about to expire, big charges coming, cards about to expire with what depends on them, unconfirmed charges and subscriptions marked as not used.
func (*Service) Bootstrap ¶
Bootstrap creates the household, its owner and default categories if no household exists yet. If one exists it returns its owner. It returns the owner as a system-usable actor.
func (*Service) BuildContext ¶
BuildContext writes a live household briefing for AI agents: who lives here, what is subscribed, what is due, what needs attention, and the rules of the domain.
func (*Service) BulkUpdate ¶
func (s *Service) BulkUpdate(ctx context.Context, actor Actor, in BulkUpdateInput) (BulkResult, error)
BulkUpdate changes the category, payment method or tags of several subscriptions, or pauses, resumes, cancels or deletes them, in one transaction: all of them or none.
func (*Service) Calendar ¶
func (s *Service) Calendar(ctx context.Context, actor Actor, in CalendarInput) (CalendarView, error)
Calendar returns every dated event between two days: charges (real ones and, further ahead, the ones the schedule will produce), free trials ending, things expiring and last days to cancel.
func (*Service) Card ¶
Card returns what a card-expiry message about pmID says to the actor. ok is false when it no longer applies: the card was renewed or deleted, or nothing uses it any more.
func (*Service) ChangeChargeAmount ¶
func (s *Service) ChangeChargeAmount(ctx context.Context, actor Actor, in ChangeChargeAmountInput) (ChargeResult, error)
ChangeChargeAmount changes one charge's amount or the price from this charge on.
func (*Service) ChangeChargeDate ¶
func (s *Service) ChangeChargeDate(ctx context.Context, actor Actor, in ChangeChargeDateInput) (ChargeResult, error)
ChangeChargeDate moves one charge ("this") or re-anchors the schedule from this charge on ("following").
func (*Service) CreateCategory ¶
func (s *Service) CreateCategory(ctx context.Context, actor Actor, in CreateCategoryInput) (domain.Category, error)
CreateCategory adds a category.
func (*Service) CreateSubscription ¶
func (s *Service) CreateSubscription(ctx context.Context, actor Actor, in CreateSubscriptionInput) (SubscriptionDetail, error)
CreateSubscription creates a subscription and its upcoming charges.
func (*Service) DecideRenewal ¶
func (s *Service) DecideRenewal(ctx context.Context, actor Actor, in DecideRenewalInput) (ChargeResult, error)
DecideRenewal records that the member wants to keep a renewal (or takes that back), so the "keep or cancel?" reminders stop. To cancel instead, use SetSubscriptionStatus.
func (*Service) DeleteCategory ¶
func (s *Service) DeleteCategory(ctx context.Context, actor Actor, in DeleteCategoryInput) (DeleteResult, error)
DeleteCategory removes a category; its subscriptions become uncategorized.
func (*Service) DeleteDocument ¶
func (s *Service) DeleteDocument(ctx context.Context, actor Actor, in DeleteDocumentInput) (DocumentResult, error)
DeleteDocument removes a document. It is kept 30 days, so undo can bring it back, then purged.
func (*Service) DeletePaymentMethod ¶
func (s *Service) DeletePaymentMethod(ctx context.Context, actor Actor, in DeletePaymentMethodInput) (ReassignResult, error)
DeletePaymentMethod deletes a payment method, moving its subscriptions to another one first when asked.
func (*Service) DeleteSubscription ¶
func (s *Service) DeleteSubscription(ctx context.Context, actor Actor, in DeleteSubscriptionInput) (DeleteResult, error)
DeleteSubscription removes a subscription (it can be restored with Undo).
func (*Service) DeleteTag ¶
func (s *Service) DeleteTag(ctx context.Context, actor Actor, in DeleteTagInput) (BulkResult, error)
DeleteTag removes a tag from every subscription the actor can see.
func (*Service) DismissDuplicate ¶
func (s *Service) DismissDuplicate(ctx context.Context, actor Actor, in DismissDuplicateInput) (DismissResult, error)
DismissDuplicate records that paying a service more than once is on purpose: the group is no longer reported by audit and nobody is messaged about it again. A group that changes (a third subscription joins it) is a new group.
func (*Service) DocumentFile ¶
func (s *Service) DocumentFile(ctx context.Context, actor Actor, id string) (Document, []byte, error)
DocumentFile returns a document and its content.
func (*Service) Duplicate ¶
func (s *Service) Duplicate(ctx context.Context, actor Actor, key string) (DuplicateNotice, bool, error)
Duplicate returns the duplicate group with key as the actor sees it. ok is false when it no longer exists or was said to be fine.
func (*Service) Export ¶
Export returns the household's data: reference data, every subscription the actor can see and all its charges.
func (*Service) FileData ¶
FileData returns the content of a live file (a document or a logo) of a subscription the actor can see, for the full export.
func (*Service) GetHousehold ¶
GetHousehold returns the household reference data.
func (*Service) GetKeepAwake ¶
GetKeepAwake returns the keep-awake setting and whether it is in effect.
func (*Service) GetSubscription ¶
func (s *Service) GetSubscription(ctx context.Context, actor Actor, in GetSubscriptionInput) (SubscriptionDetail, error)
GetSubscription returns a subscription with upcoming and past charges.
func (*Service) Glance ¶ added in v0.4.0
Glance returns the next charges, what the next 30 days cost and whether this computer is kept awake.
func (*Service) Import ¶
func (s *Service) Import(ctx context.Context, actor Actor, in ImportInput) (ImportResult, error)
Import creates many subscriptions in one step. Items that fail validation or duplicate an existing subscription are skipped and reported; the rest are created. With DryRun it is a review list: nothing is saved.
func (*Service) KeepAwakeSession ¶ added in v0.4.0
func (s *Service) KeepAwakeSession(ctx context.Context, actor Actor, in KeepAwakeSessionInput) (KeepAwakeInfo, error)
KeepAwakeSession starts, extends or stops a session that keeps the computer awake now. Owners only.
func (*Service) ListAudit ¶
ListAudit returns recent changes made by members, agents and the system.
func (*Service) ListCharges ¶
func (s *Service) ListCharges(ctx context.Context, actor Actor, in ListChargesInput) (ChargeList, error)
ListCharges returns charges due in a date range, e.g. the next 30 days.
func (*Service) ListDocuments ¶
func (s *Service) ListDocuments(ctx context.Context, actor Actor, in ListDocumentsInput) (DocumentList, error)
ListDocuments returns the documents attached to a subscription the actor can see.
func (*Service) ListSubscriptions ¶
func (s *Service) ListSubscriptions(ctx context.Context, actor Actor, in ListSubscriptionsInput) (SubscriptionList, error)
ListSubscriptions returns the subscriptions visible to the actor.
func (*Service) Logo ¶
func (s *Service) Logo(ctx context.Context, actor Actor, subscriptionID string) (Document, []byte, error)
Logo returns the uploaded logo of a subscription the actor can see.
func (*Service) MarkDelivered ¶
MarkDelivered records a successful delivery.
func (*Service) MarkFailed ¶
MarkFailed records a failed attempt and schedules a retry with backoff.
func (*Service) MarkObsolete ¶
func (s *Service) MarkObsolete(ctx context.Context, item OutboxItem) error
MarkObsolete records that an item was not sent because it no longer applies.
func (*Service) MarkUndeliverable ¶
MarkUndeliverable records that an item could not be sent to anyone and will not be retried: there is nowhere to send it.
func (*Service) MatchStatement ¶
func (s *Service) MatchStatement(ctx context.Context, actor Actor, in MatchStatementInput) (StatementMatch, error)
MatchStatement compares the transactions of a statement with the charges Exhale expects in the same period. It changes nothing.
func (*Service) MonthBalances ¶
func (s *Service) MonthBalances(ctx context.Context, actor Actor, in BalancesInput) (Balances, error)
MonthBalances computes who owes whom for the charges of one month of the subscriptions that have cost shares. The payer of a subscription is who actually paid; shares say who should carry it.
func (*Service) NeedsAttention ¶
func (s *Service) NeedsAttention(ctx context.Context, actor Actor, _ struct{}) (AttentionList, error)
NeedsAttention lists unconfirmed past charges and expiring payment methods.
func (*Service) NextOutboxDue ¶
NextOutboxDue returns when the next pending item is due, if any.
func (*Service) Owner ¶
Owner returns the first owner of the household as a trusted actor, or ErrNotFound if no household exists yet.
func (*Service) Plan ¶
Plan materializes upcoming charges for every active subscription, enqueues their reminders and confirmation questions in the outbox, and auto-confirms charges of trusted subscriptions. It is idempotent: every outbox entry has a dedupe key, so running it again never duplicates a reminder (ADR 0003).
func (*Service) PreviewSchedule ¶
func (s *Service) PreviewSchedule(_ context.Context, _ Actor, in PreviewScheduleInput) (SchedulePreview, error)
PreviewSchedule computes charge dates with the billing engine. The web calls it while a recurrence is being edited, so dates are never computed in two places (ADR 0013).
func (*Service) PurgeDeletedFiles ¶
PurgeDeletedFiles removes for good the files deleted more than FileRetention ago. The scheduler calls it every hour.
func (*Service) ReassignPaymentMethod ¶
func (s *Service) ReassignPaymentMethod(ctx context.Context, actor Actor, in ReassignPaymentMethodInput) (ReassignResult, error)
ReassignPaymentMethod moves all subscriptions paid with one method to another: the one-step answer to "this card expires next month".
func (*Service) Reminders ¶
func (s *Service) Reminders(ctx context.Context, actor Actor, in ReminderLogInput) (ReminderLog, error)
Reminders returns the reminders of the household, newest first, each with what happened to it: sent, waiting, failed and why, or no longer needed.
func (*Service) RemoveLogo ¶
func (s *Service) RemoveLogo(ctx context.Context, actor Actor, in RemoveLogoInput) (LogoResult, error)
RemoveLogo drops an uploaded logo: the subscription shows the bundled brand mark or its monogram again.
func (*Service) RenameTag ¶
func (s *Service) RenameTag(ctx context.Context, actor Actor, in RenameTagInput) (BulkResult, error)
RenameTag renames a tag everywhere the actor can see it.
func (*Service) SavePaymentMethod ¶
func (s *Service) SavePaymentMethod(ctx context.Context, actor Actor, in SavePaymentMethodInput) (domain.PaymentMethod, error)
SavePaymentMethod creates or updates a payment method.
func (*Service) SetChargeStatus ¶
func (s *Service) SetChargeStatus(ctx context.Context, actor Actor, in SetChargeStatusInput) (ChargeResult, error)
SetChargeStatus marks a charge paid, failed, skipped, or reopens it.
func (*Service) SetKeepAwake ¶
func (s *Service) SetKeepAwake(ctx context.Context, actor Actor, in SetKeepAwakeInput) (KeepAwakeInfo, error)
SetKeepAwake changes when the computer Exhale runs on is kept awake. Owners only.
func (*Service) SetLogo ¶
func (s *Service) SetLogo(ctx context.Context, actor Actor, in SetLogoInput) (LogoResult, error)
SetLogo makes an uploaded picture the subscription's logo. The picture is decoded and drawn again as a PNG of at most 256×256: whatever else the file carried (metadata, a script, a second format hidden inside) is not kept. SVG is refused (ADR 0027).
func (*Service) SetSubscriptionStatus ¶
func (s *Service) SetSubscriptionStatus(ctx context.Context, actor Actor, in SetSubscriptionStatusInput) (SubscriptionDetail, error)
SetSubscriptionStatus changes the lifecycle of a subscription. Cancelling at period end keeps the service until the day before the next charge.
func (*Service) SetupLidClosed ¶ added in v0.4.0
func (s *Service) SetupLidClosed(ctx context.Context, actor Actor, in SetupLidClosedInput) (KeepAwakeInfo, error)
SetupLidClosed asks this computer for the permission to keep it awake with its lid closed: on a Mac, the administrator's password, in a dialog on its screen, to allow `pmset disablesleep` (ADR 0032). Owners only.
func (*Service) Snooze ¶
func (s *Service) Snooze(ctx context.Context, actor Actor, in SnoozeInput) (SnoozeResult, error)
Snooze brings a reminder, a "keep or cancel?" or a "was it charged?" back in a few days at the reminder time, never past the point where it is useless: a decision snoozed past the last day to cancel comes back on that day.
func (*Service) SpendBreakdown ¶
func (s *Service) SpendBreakdown(ctx context.Context, actor Actor, period, by string, ids []string) (Breakdown, error)
SpendBreakdown computes a Breakdown of the active subscriptions the actor can see; ids, when given, keep only those subscriptions.
func (*Service) SpendHistory ¶
func (s *Service) SpendHistory(ctx context.Context, actor Actor, in SpendingHistoryInput) (SpendingHistory, error)
SpendHistory returns what was really paid each month (from the charge ledger, not from current prices) and what is expected in the coming months.
func (*Service) Store ¶
Store returns the underlying store (for adapters that need raw queries, such as message bookkeeping).
func (*Service) Summary ¶
Summary returns recurring spend per month and year (per currency and converted into the main currency), per category and per payment method, plus the charges of the next 7 days.
func (*Service) Undo ¶
Undo reverts a recorded change by restoring the entity's previous state (ADR 0018: every agent change is reversible).
func (*Service) UpdateCategory ¶
func (s *Service) UpdateCategory(ctx context.Context, actor Actor, in UpdateCategoryInput) (domain.Category, error)
UpdateCategory renames or recolours a category.
func (*Service) UpdateHousehold ¶
func (s *Service) UpdateHousehold(ctx context.Context, actor Actor, in UpdateHouseholdInput) (HouseholdInfo, error)
UpdateHousehold changes the display currency, time zone, language, name or default reminder time. Owners only.
func (*Service) UpdateSubscription ¶
func (s *Service) UpdateSubscription(ctx context.Context, actor Actor, in UpdateSubscriptionInput) (SubscriptionDetail, error)
UpdateSubscription edits a subscription.
type SetChargeStatusInput ¶
type SetChargeStatusInput struct {
ChargeID string `json:"chargeId"`
Status string `json:"status" jsonschema:"paid, failed, skipped, or pending to reopen" enum:"paid,failed,skipped,pending"`
ChargedAmount string `json:"chargedAmount,omitempty" jsonschema:"what the bank actually took, decimal string (optional)"`
ChargedIn string `json:"chargedCurrency,omitempty" jsonschema:"currency of chargedAmount, e.g. EUR when a USD service is paid from a EUR card"`
Note string `json:"note,omitempty"`
DryRun bool `json:"dryRun,omitempty"`
}
SetChargeStatusInput resolves a charge.
type SetKeepAwakeInput ¶
type SetKeepAwakeInput struct {
Mode *string `` /* 163-byte string literal not displayed */
From *string `json:"from,omitempty" jsonschema:"start of the hours, HH:MM in the household time zone"`
To *string `` /* 126-byte string literal not displayed */
KeepDisplayOn *bool `json:"keepDisplayOn,omitempty" jsonschema:"also keep the display on; false lets it sleep while the system stays awake"`
LidClosed *bool `json:"lidClosed,omitempty" jsonschema:"keep a laptop awake with its lid closed too (on a Mac, needs setup_lid_closed once)"`
OnlyOnPower *bool `json:"onlyOnPower,omitempty" jsonschema:"only while plugged in"`
BatteryFloor *int `` /* 134-byte string literal not displayed */
DryRun bool `json:"dryRun,omitempty"`
}
SetKeepAwakeInput changes when the computer is kept awake. Only the fields you pass change.
type SetLogoInput ¶
SetLogoInput replaces a subscription's logo with an uploaded picture.
type SetSubscriptionStatusInput ¶
type SetSubscriptionStatusInput struct {
ID string `json:"id"`
Action string `` /* 128-byte string literal not displayed */
DryRun bool `json:"dryRun,omitempty"`
}
SetSubscriptionStatusInput pauses, resumes or cancels a subscription.
type SetupLidClosedInput ¶ added in v0.4.0
type SetupLidClosedInput struct {
DryRun bool `json:"dryRun,omitempty"`
}
SetupLidClosedInput asks this computer for the permission to keep it awake with its lid closed.
type SnoozeInput ¶
type SnoozeInput struct {
ChargeID string `json:"chargeId"`
Kind string `` /* 200-byte string literal not displayed */
Days int `json:"days" jsonschema:"bring it back in this many days, at the reminder time (1: tomorrow)" minimum:"1" maximum:"30"`
DryRun bool `json:"dryRun,omitempty"`
}
SnoozeInput asks for a reminder again later.
type SnoozeResult ¶
type SnoozeResult struct {
ChargeID string `json:"chargeId"`
Kind string `json:"kind"`
On engine.Date `json:"on" jsonschema:"the day it comes back"`
At time.Time `json:"at" jsonschema:"when it comes back"`
Clamped bool `` /* 130-byte string literal not displayed */
Planned bool `json:"alreadyPlanned" jsonschema:"a message about this charge was already planned that day, so no second one is added"`
DryRun bool `json:"dryRun,omitempty"`
}
SnoozeResult says when the message comes back.
type SpendingHistory ¶
type SpendingHistory struct {
MainCurrency string `json:"mainCurrency"`
Months []MonthSpend `json:"months" jsonschema:"oldest first; past months show what was paid, future months what is expected"`
BySubscription []SubscriptionSpend `json:"bySubscription" jsonschema:"lifetime paid per subscription, largest first"`
TotalPaid money.Money `json:"totalPaid" jsonschema:"everything marked paid in the months shown"`
HeaviestMonth string `json:"heaviestMonth,omitempty" jsonschema:"upcoming month with the most to pay, YYYY-MM"`
Unconverted []string `json:"unconverted" jsonschema:"currencies without a known rate, left out"`
}
SpendingHistory is real spend per month and per subscription.
type SpendingHistoryInput ¶
type SpendingHistoryInput struct {
Months int `json:"months,omitempty" query:"months" jsonschema:"months to look back and ahead, default 12" minimum:"1" maximum:"60"`
}
SpendingHistoryInput selects how many months to look back and ahead.
type SpendingSummary ¶
type SpendingSummary struct {
MainCurrency string `json:"mainCurrency"`
Monthly []money.Money `json:"monthlyByCurrency" jsonschema:"active subscriptions normalized per month, one entry per currency"`
Yearly []money.Money `json:"yearlyByCurrency"`
// MonthlyTotal and YearlyTotal are in the main currency, converting other
// currencies at the latest ECB reference rate (see RatesDate).
MonthlyTotal money.Money `json:"monthlyTotal"`
YearlyTotal money.Money `json:"yearlyTotal"`
Converted bool `json:"converted" jsonschema:"true when other currencies were converted into the main currency"`
Unconverted []string `json:"unconverted" jsonschema:"currencies without a known rate, left out of the totals"`
Rates map[string]string `json:"rates" jsonschema:"units of the main currency per 1 unit of each currency"`
RatesDate string `json:"ratesDate,omitempty" jsonschema:"ECB reference date of the rates used"`
ByCategory []CategorySpend `json:"byCategory" jsonschema:"monthly spend per category, in the main currency"`
ByMethod []MethodSpend `json:"byPaymentMethod" jsonschema:"monthly spend per payment method, in the main currency"`
Active int `json:"activeSubscriptions"`
NextCharges []ChargeView `json:"next7Days"`
// SetAside is the monthly equivalent of everything that is not billed
// monthly: what to put away each month so yearly bills do not surprise.
SetAside money.Money `` /* 183-byte string literal not displayed */
// BeforePayday is set when the household has a payday.
BeforePayday *PaydayOutlook `json:"beforePayday,omitempty" jsonschema:"what is due before the next payday, when the household has one"`
// Stopped is what cancelling saved: "you stopped paying X per year".
Stopped StoppedSpend `` /* 249-byte string literal not displayed */
}
SpendingSummary totals recurring spend.
type StatementLine ¶
type StatementLine struct {
Date string `json:"date" jsonschema:"when the money left, YYYY-MM-DD"`
Description string `json:"description" jsonschema:"the text the bank shows, e.g. NETFLIX.COM 866-579-7172"`
Amount string `` /* 129-byte string literal not displayed */
Currency string `json:"currency,omitempty" jsonschema:"ISO 4217 code; default: the household's currency"`
}
StatementLine is one outgoing payment of a bank or card statement.
type StatementMatch ¶
type StatementMatch struct {
From engine.Date `json:"from"`
To engine.Date `json:"to"`
Matches []ChargeMatch `json:"matches" jsonschema:"expected charges found in the statement, by due date"`
// Missing are open charges due in the period that no transaction
// matches.
Missing []ChargeView `` /* 220-byte string literal not displayed */
Unmatched int `json:"unmatched" jsonschema:"transactions that match no expected charge"`
}
StatementMatch says which expected charges a statement confirms.
type StoppedSpend ¶
type StoppedSpend struct {
Yearly money.Money `json:"yearly" jsonschema:"what the cancelled subscriptions would cost per year at their last price, in the display currency"`
Count int `json:"count"`
Subscriptions []StoppedSubscription `json:"subscriptions" jsonschema:"most recently ended first"`
}
StoppedSpend is what the household no longer pays because it cancelled subscriptions.
type StoppedSubscription ¶
type StoppedSubscription struct {
ID string `json:"id"`
Name string `json:"name"`
Yearly money.Money `json:"yearly" jsonschema:"per year at its last price, in the subscription currency"`
// YearlyMain is Yearly converted into the display currency.
YearlyMain money.Money `json:"yearlyMain" jsonschema:"per year, in the display currency"`
Until engine.Date `json:"until,omitzero" jsonschema:"last day of service"`
}
StoppedSubscription is one cancelled subscription and what it cost.
type SubscriptionDetail ¶
type SubscriptionDetail struct {
Subscription SubscriptionView `json:"subscription"`
Upcoming []ChargeView `json:"upcoming"`
History []ChargeView `json:"history"`
PriceHistory []PricePoint `json:"priceHistory" jsonschema:"every price it has had, oldest first, from the charge ledger; a scheduled change comes last"`
Paid PaidTotals `json:"paid" jsonschema:"what it has really cost so far"`
}
SubscriptionDetail is a subscription with its upcoming and past charges.
type SubscriptionList ¶
type SubscriptionList struct {
Subscriptions []SubscriptionView `json:"subscriptions"`
}
SubscriptionList is a list of subscriptions.
type SubscriptionRef ¶
type SubscriptionRef struct {
ID string `json:"id"`
Name string `json:"name"`
Payer string `json:"payer,omitempty"`
Monthly money.Money `json:"monthly" jsonschema:"in the subscription currency"`
}
SubscriptionRef names a subscription in a finding.
type SubscriptionSpend ¶
type SubscriptionSpend struct {
ID string `json:"id"`
Name string `json:"name"`
Paid money.Money `json:"paid" jsonschema:"sum of paid charges, in the display currency"`
Count int `json:"count"`
Since engine.Date `json:"since,omitzero"`
}
SubscriptionSpend is what one subscription has really cost.
type SubscriptionView ¶
type SubscriptionView struct {
domain.Subscription
Cadence string `json:"cadence" jsonschema:"human description of the billing cycle, e.g. every 1 month"`
NextCharge engine.Date `json:"nextCharge,omitzero" jsonschema:"next charge date (YYYY-MM-DD), empty if none"`
MonthlyCost money.Money `json:"monthlyCost" jsonschema:"price normalized to one month, in the subscription currency"`
CategoryName string `json:"categoryName,omitempty"`
PaymentMethod string `json:"paymentMethod,omitempty" jsonschema:"payment method label, e.g. Revolut ••0937"`
PayerName string `json:"payerName,omitempty"`
CancelledUntil engine.Date `json:"serviceUntil,omitzero" jsonschema:"for cancelled subscriptions: last day of service"`
YearlyCost money.Money `json:"yearlyCost" jsonschema:"price normalized to one year, in the subscription currency"`
InTrial bool `json:"inTrial" jsonschema:"the free trial is still running; the next charge is the first paid one"`
CancelBy engine.Date `` /* 155-byte string literal not displayed */
Expires bool `json:"expires" jsonschema:"manual renewal: the next charge date is when it expires unless renewed"`
AskUsage bool `` /* 127-byte string literal not displayed */
// PlanSwitch is set only when the member recorded another price (altPrice).
PlanSwitch *PlanSwitch `` /* 146-byte string literal not displayed */
// Billing rules (weekday of the month, weekend shift).
BillingDay string `` /* 206-byte string literal not displayed */
NextScheduled engine.Date `` /* 156-byte string literal not displayed */
Logo string `json:"logo,omitempty" jsonschema:"path of a logo someone uploaded for it; empty: the bundled brand mark or the initials"`
}
SubscriptionView is a subscription plus everything a reader (human or AI agent) needs to understand it without extra lookups.
type TermsInput ¶
type TermsInput struct {
StartedOn string `json:"startedOn,omitempty" jsonschema:"when the member first subscribed, YYYY-MM-DD. History only: it creates no charges"`
TrialEndsOn string `` /* 146-byte string literal not displayed */
NoticeDays int `` /* 176-byte string literal not displayed */
Renewal string `` /* 189-byte string literal not displayed */
CancelURL string `json:"cancelUrl,omitempty" jsonschema:"page where it is cancelled or managed, https://…"`
Account string `json:"account,omitempty" jsonschema:"login or email the subscription lives under"`
}
TermsInput are the contract facts that can be set when creating a subscription.
type Transfer ¶
type Transfer struct {
From string `json:"from" jsonschema:"member id who pays"`
To string `json:"to" jsonschema:"member id who receives"`
Amount money.Money `json:"amount"`
Text string `json:"text" jsonschema:"e.g. Ana pays Alex 12.50 EUR"`
}
Transfer settles part of the month between two members.
type UndoInput ¶
type UndoInput struct {
AuditID string `json:"auditId,omitempty" jsonschema:"id returned by a write (aud_…)"`
// AuditIDs revert several changes together, e.g. every change of a bulk
// edit: all of them or none.
AuditIDs []string `` /* 134-byte string literal not displayed */
DryRun bool `json:"dryRun,omitempty"`
}
UndoInput identifies the change to revert.
type UndoResult ¶
type UndoResult struct {
Restored string `json:"restored" jsonschema:"entity id that was restored"`
AuditID string `json:"auditId"`
// Undone counts the changes reverted (more than one with auditIds).
Undone int `json:"undone"`
}
UndoResult reports what was restored.
type UnusedSubscription ¶
type UnusedSubscription struct {
Subscription SubscriptionRef `json:"subscription"`
Yearly money.Money `json:"yearly" jsonschema:"what it costs per year, in the subscription currency"`
AnsweredOn engine.Date `json:"answeredOn"`
}
UnusedSubscription is one the member said they no longer use.
type UpdateCategoryInput ¶
type UpdateCategoryInput struct {
ID string `json:"id" jsonschema:"category id (cat_…)"`
Name *string `json:"name,omitempty"`
Color *string `json:"color,omitempty" jsonschema:"hex color, e.g. #7c6cff"`
DryRun bool `json:"dryRun,omitempty"`
}
UpdateCategoryInput renames or recolours a category.
type UpdateHouseholdInput ¶
type UpdateHouseholdInput struct {
Name *string `json:"name,omitempty" maxLength:"60"`
Currency *string `json:"currency,omitempty" jsonschema:"display currency (ISO 4217): totals are converted into it"`
Timezone *string `json:"timezone,omitempty" jsonschema:"IANA time zone, e.g. Europe/Madrid"`
Locale *string `` /* 140-byte string literal not displayed */
ReminderTime *string `` /* 141-byte string literal not displayed */
Payday *int `` /* 186-byte string literal not displayed */
// Repeat until handled: how often an unanswered "was it charged?" comes back.
ConfirmTimes *int `` /* 221-byte string literal not displayed */
ConfirmEveryDays *int `json:"confirmEveryDays,omitempty" jsonschema:"days between two of those questions" minimum:"1" maximum:"30"`
DryRun bool `json:"dryRun,omitempty"`
}
UpdateHouseholdInput changes household preferences. Only the fields you pass change.
type UpdateSubscriptionInput ¶
type UpdateSubscriptionInput struct {
ID string `json:"id" jsonschema:"subscription id (sub_…)"`
Name *string `json:"name,omitempty"`
Price *string `json:"price,omitempty" jsonschema:"new price from the next open charge on, decimal string"`
CategoryID *string `json:"categoryId,omitempty"`
PaymentMethodID *string `json:"paymentMethodId,omitempty" jsonschema:"applies to this and following charges"`
PayerID *string `json:"payerId,omitempty"`
Private *bool `json:"private,omitempty"`
URL *string `json:"url,omitempty"`
Notes *string `json:"notes,omitempty"`
AutoConfirm *bool `json:"autoConfirm,omitempty"`
ReminderDays []int `json:"reminderDays,omitempty"`
ReminderHour *int `json:"reminderHour,omitempty" jsonschema:"local hour of day for reminders (0-23)"`
Interval *int `json:"interval,omitempty" jsonschema:"new number of units between charges, from the next charge on" minimum:"1"`
Unit *string `json:"unit,omitempty" jsonschema:"new billing unit, from the next charge on" enum:"day,week,month,year"`
NextCharge *string `` /* 162-byte string literal not displayed */
DayRule *string `` /* 470-byte string literal not displayed */
WeekendShift *string `` /* 343-byte string literal not displayed */
EndDate *string `` /* 126-byte string literal not displayed */
PriceFrom *string `` /* 170-byte string literal not displayed */
StartedOn *string `json:"startedOn,omitempty" jsonschema:"when the member first subscribed, YYYY-MM-DD; empty string clears it"`
TrialEndsOn *string `` /* 129-byte string literal not displayed */
NoticeDays *int `json:"noticeDays,omitempty" jsonschema:"days of notice required to cancel before a renewal; 0 removes it"`
Renewal *string `json:"renewal,omitempty" jsonschema:"auto or manual (expires unless renewed)" enum:"auto,manual"`
CancelURL *string `json:"cancelUrl,omitempty" jsonschema:"page where it is cancelled or managed"`
Account *string `json:"account,omitempty" jsonschema:"login or email the subscription lives under"`
// Shares replace the cost split; an empty list gives the cost back to the payer.
Using *bool `json:"using,omitempty" jsonschema:"the member's answer to 'still using this?'"`
ExtrasUpdate
DryRun bool `json:"dryRun,omitempty"`
}
UpdateSubscriptionInput changes subscription-level fields. Only non-nil fields change. Price changes made here apply from the next open charge on ("this and following"); use ChangeChargeAmount for a single charge.