56 Commits

Author SHA1 Message Date
Donal McBreen 98d06947cb Implement Queenbee account callbacks
Add Account::QueenbeeIntegration concern (SaaS-only) that bridges
Fizzy's external_account_id to Queenbee's find_by_queenbee_id lookup,
and adapts the bang lifecycle methods to Fizzy's Cancellable module.

Remove dead billing conditional from cancellation mailer — Fizzy is
free-only, there's no charge to stop.
2026-03-31 15:14:49 +01:00
Jorge Manrubia 1c4bf3e1e0 Add per-account storage exceptions for SaaS (#2733)
Allow granting specific accounts more storage via an
account_storage_exceptions table in the saas DB, without
relying on the blanket staff bypass.
2026-03-19 15:36:36 +01:00
Jorge Manrubia cf10dbd91e Add 1GB storage cap with staff bypass (#2729)
Re-applies the storage limit from #2713 (reverted in #2715) with an
exception for staff identities so our own account isn't blocked.
2026-03-19 12:38:39 +01:00
Jorge Manrubia 0be8041953 Revert "Add 1GB storage cap with self-host notice" 2026-03-17 17:51:07 +01:00
Jorge Manrubia a761d809ae Add 1GB storage limit with self-host notice for SaaS
Limit free storage to 1GB on the SaaS version. When exceeded, card
publishing, comment creation, and JSON card creation are blocked,
and the card footer shows a "self-host Fizzy for unlimited storage"
notice instead of the create buttons. A nearing-limit warning
appears when usage exceeds 500MB.

Uses the same SaaS engine patterns as the removed billing system:
model concern on Account, controller concerns included via
config.to_prepare, view partials in saas/ with Fizzy.saas? guards
in the main app.
2026-03-17 12:21:32 +01:00
Jorge Manrubia 964915bc66 Remove payment/subscription system and card/storage limits
Fizzy is now free. Remove the entire Stripe billing system,
subscription management, and card/storage limit enforcement.

Removes from saas/: Plan model, Account::Billing, Account::Subscription,
Account::Limited, Account::OverriddenLimits, Account::BillingWaiver,
all subscription/billing controllers and views, Stripe webhook handler,
card creation/publishing limit enforcement, admin account override UI,
usage report rake task, and all related tests.

Removes from main app: Fizzy.saas? guards for subscription panel,
SaaS card footer override, near-limit notices, and saas.css stylesheet.

Adds migration to drop billing tables from the SaaS database.

Non-billing SaaS features (push notifications, signup, authorization,
telemetry, console1984/audits1984) are preserved.
2026-03-17 09:22:04 +01:00
Mike Dalessio ab5283441d SaaS usage reporting (#2690)
* Add saas:usage_report rake task and extract Subscription.paid scope

Add a rake task to generate a CSV usage report with per-account data:
Queenbee ID, sign up date, paid date, card count, storage used, and
last active date.

Extract the paid subscriptions query from Admin::StatsController into
an Account::Subscription.paid scope so both the controller and the
new rake task can share it.

* Add comped and account name columns to usage report

* Update saas/lib/tasks/fizzy/usage_report.rake

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Preload storage_total to avoid N+1 in usage report

* Batch last_active_at queries to avoid per-account aggregates

* Add tests for Account::Subscription.paid scope

* Update saas/app/models/account/subscription.rb

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Aggregate paid dates in SQL instead of loading all subscriptions

* Fix paid scope to derive plan keys from Plan.all instead of PLANS hash

* Move paid dates and comped lookups into per-batch queries

* Materialize batch IDs to avoid cross-database subquery

SaasRecord models live on a separate database (fizzy_saas) that doesn't
have the accounts table. Using batch.select(:id) generated a subquery
that ran on the saas database, causing a table-not-found error. Using
pluck(:id) materializes the IDs into an array instead.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 10:11:13 -04:00
Andy Smith 9cd1167f85 Change exceeding and nearing limits to account for the 1000th card (#2345)
* Change exceeding and nearing limits to account for the 1000th card (instead of 1001)

* Add boundary condition tests for card limit checks

Tests now verify behavior at exactly the limit (1000 cards) and at the
nearing threshold (remaining = 100), covering the off-by-one fix.

---------

Co-authored-by: Jorge Manrubia <jorge@37signals.com>
2026-03-03 10:28:38 +01:00
Jorge Manrubia f6952187c7 Merge pull request #2508 from henrik/patch-1
billing.rb: destroy -> destroy!
2026-03-03 10:25:48 +01:00
Rosa Gutierrez 6fb24118ea Use internal account ID as account_id and add account_slug
In native push payload, to be consistent about how we refer to
this in other endpoints.
2026-02-25 19:31:13 +01:00
Fernando Olivares cefc20612a Add creator_familiar_name to push notification payload
Include the shortened familiar name format (e.g., "Salvador D.")
for display in iOS notification titles.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Fernando Olivares b1cdb72381 Add creator initials and avatar color to push notification payload
Move avatar_background_color logic from helper to User::Avatar concern
so it can be accessed from models. Include creator_id, creator_initials,
and creator_avatar_color in native push notifications for local avatar
rendering on iOS.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Fernando Olivares 113e61d418 Add avatar_url so we always get an avatar even when the user hasn't set one 2026-02-25 19:31:13 +01:00
Rosa Gutierrez 7864748be9 Simplify PushTarget by removing template method pattern
Each target now implements process directly with its own logic,
rather than using processable?/perform_push hooks. The pushable?
check is done once in Notification#push before iterating targets.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 3621df8f0f Consolidate push jobs into single Notification::PushJob
Replace separate WebPushJob and NativePushJob with a single PushJob
that calls notification.push, which iterates over registered targets.
Each target handles its own delivery - Web pushes synchronously via
the pool, Native enqueues device-level jobs via deliver_later_to.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 4f19c42958 Move category and high_priority to payload classes
Use polymorphism instead of case statements in Native push target:
- DefaultPayload#category returns "default", #high_priority? returns false
- EventPayload#category returns "assignment"/"comment"/"card" based on action
- MentionPayload#category returns "mention", #high_priority? returns true

This simplifies the Native push target by delegating source-specific
logic to the appropriate payload classes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 9299300dbf Move push priority concerns from Event and Mention into Native push target 2026-02-25 19:31:13 +01:00
Fernando Olivares c7d0559191 Change priority notification level for mentions and assignments 2026-02-25 19:31:13 +01:00
Rosa Gutierrez ab6bc256eb Link devices to sessions for automatic cleanup on logout
When a session is destroyed (user logs out), all devices registered
to that session are automatically destroyed, preventing push
notifications from being sent to logged-out devices.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Rosa Gutierrez b5205ce6b2 Rename Push to PushTarget for better readability
PushTarget#push reads more naturally than Push#push. The push target
is the thing that pushes notifications to a specific destination.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 06c9ece058 Move payload method to Notification and make accessors public
The notification now owns its payload via #payload method in Pushable,
allowing direct access like notification.payload.title. Push classes
simply use the notification's payload rather than building it themselves.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 05819f84a2 Refactor notification push system with registry pattern
Replace NotificationPusher with a cleaner architecture:

- Add Notification::Pushable concern with push target registry
- Add Notification::Push base class with template methods
- Add Notification::Push::Web for web push (OSS)
- Add Notification::Push::Native for native push (SaaS)
- Add Notification::WebPushJob and Notification::NativePushJob

Key design:
- Registry pattern: Notification.register_push_target(:web)
- Template method: push calls should_push? then perform_push
- Subclasses override should_push? (with super) and perform_push
- Each target handles its own job enqueueing

Also:
- Add Notification#pushable? for checking push eligibility
- Add Notification#identity delegation to user
- Reorganize tests to match new class structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Tidy up saas engine a bit more
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 555132bbef Change device ownership from User to Identity
Devices now belong to Identity instead of User, allowing a single
device registration to work across all accounts an identity has
access to.

- Move User::Devices to Identity::Devices
- Update DevicesController to use Current.identity
- Update NotificationPusher::Native to use user.identity.devices
- Clean up tests to use @identity directly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Fix reference to `user.devices`, left-over from the identity switch
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 9d32f3e845 Refactor devices controller and extract registration to model
- Remove Users namespace from DevicesController (now just DevicesController)
- Create ApplicationPushDevice model extending ActionPushNative::Device
- Move device registration logic (find_or_initialize + update) to model
- Update User::Devices concern to use ApplicationPushDevice
- Fix push notification tests (endpoint validation, job count expectations)
- Update push_config_test to use ActionPushNative.config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Fernando Olivares 7e0470a692 Send the URL instead of path in notifications 2026-02-25 19:31:13 +01:00
Fernando Olivares ade39b15ab Add title/body to android notifications too 2026-02-25 19:31:13 +01:00
Fernando Olivares 55336873b2 Fix database issues and add APNS configuration
- Fix owner_id type to UUID in devices migration
- Fix NOT NULL crash in device registration
- Add APNS config and 1Password integration
- Add --apns flag to bin/dev for local development
- Clean up devices controller
2026-02-25 19:31:13 +01:00
Fernando Olivares 3c54cd84fc Add native push notification infrastructure
- Add action_push_native gem and SaaS configuration
- Add device registration API and UI
- Add User::Devices concern
- Add NotificationPusher::Native for push delivery
- Add tests and fixtures for native push
2026-02-25 19:31:13 +01:00
Henrik Nyh f50b0bca5a billing.rb: destroy -> destroy! 2026-02-08 19:51:41 +00:00
Jeremy Daer f2ff5af4f5 Sync email to Stripe when user changes email address (#2432)
* Sync owner email to Stripe when changed

When an account owner changes their email address, update the
corresponding Stripe customer record via a background job.

Also handles ownership transfers: when a user becomes the account
owner, their email is synced to Stripe.

Responsibility chain:
- User::NotifiesAccountOfEmailChange triggers on owner identity change
  or when a user becomes owner
- Account::Billing#owner_email_changed enqueues sync job
- Account::SyncStripeCustomerEmailJob performs the update with
  polynomial backoff retries
- Account::Subscription#sync_customer_email_to_stripe calls Stripe API

* Address PR feedback: error handling and test coverage

- Handle Stripe::InvalidRequestError in sync_customer_email_to_stripe
  (mirrors cancel method behavior for deleted customers)
- Add test for deactivated owner (owner with nil identity)
- Add test for deleted Stripe customer scenario
2026-01-25 14:20:39 -08:00
Stanko Krtalić eceb6c9b43 Add self-service account deletion (#2246)
* Add self-service account deletion

* Disable access to cancelled accounts & implement Stripe interactions

* Add tests

* Fix failing tests

* Remove cancelled accounts from lists

* Fix incorrect redirect

* Use _path instead of _url for consistency

* Don't track how far the inicieration job got

We still want the step tracking so that the job can be interrupted. But, since the scope is idempotent, we don't need to track how far it got.

* Specify the exact time when the data will be deleted

* Fix crash due to unadvancable cursor

* Rename up_for_incineration to due_for_incineration

* Regenrate the schema

* Fix incorrect path check

* Migrate the SQLite schema

* Only show the cancel button on cancellable accounts

* Check that a subscirption method exists before calling it

* Ignore job failures due to missing records when an account gets deleted

* Skip sending notifications on cancelled accounts

* Use collbacks to integrate

* Add a blank line between queue_as and discard_on

* Break checks into methods

* Inline methods

* Rename Account::IncinerateJob

* Run migrations

* Migrate SQLite
2026-01-12 14:21:05 +01:00
Jorge Manrubia b44b7ba992 Add method to locate plans by the stripe price id 2025-12-18 10:05:13 +01:00
Jason Zimdars 95bfc40392 Tweak plan name 2025-12-18 10:04:15 +01:00
Jorge Manrubia b326f78d02 More idiomatic 2025-12-18 10:04:15 +01:00
Jorge Manrubia 3b99aa5dcf Remove unused method 2025-12-18 10:04:15 +01:00
Jorge Manrubia c56dbb5f1e Add upgrade/downgrade options 2025-12-18 10:04:15 +01:00
Jorge Manrubia 9c8c56cdb8 Comped plans use the extra storage variant 2025-12-18 10:04:15 +01:00
Jorge Manrubia 823cf9fbb9 Add some methods to deal with storage limits 2025-12-18 10:04:15 +01:00
Jorge Manrubia 611e1cc97e Add additional plan for extra storage 2025-12-18 10:04:15 +01:00
Jorge Manrubia 5b2fa6b74a Remove nested if 2025-12-16 12:00:23 +01:00
Jorge Manrubia 2671e52821 Move presentation method to a regular view helper 2025-12-16 12:00:23 +01:00
Jorge Manrubia 71369de38d Extract method 2025-12-16 12:00:23 +01:00
Jorge Manrubia b39a9ecab4 Make active_subscription a private method 2025-12-16 12:00:23 +01:00
Jorge Manrubia e47657759f More concise 2025-12-16 12:00:23 +01:00
Jorge Manrubia 3395b8d6ed Grab and show the next amount to pay from Stripe 2025-12-16 12:00:23 +01:00
Jorge Manrubia 8dd31e3635 Add system to comp accounts 2025-12-16 12:00:23 +01:00
Jorge Manrubia 3ef5e4eeef Add record to track overridden limits
Before, we were relying on just changing the cards_count in account, but this
could create problems where the system to calculate the next card number fails due
to the unique constraint.
2025-12-16 12:00:23 +01:00
Jorge Manrubia f4184e5a61 Remove redundant condition 2025-12-16 12:00:23 +01:00
Jason Zimdars d556f051a6 Spelling 2025-12-16 12:00:23 +01:00
Jorge Manrubia e53455d2ca Paid plans never exceed limits! 2025-12-16 12:00:23 +01:00