41 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
Rosa Gutierrez 13268ef668 Add base_url to native push notification payload
Include the server's base URL so native apps can identify which Fizzy
instance (SaaS or self-hosted) a notification originated from.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:31:13 +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
Rosa Gutierrez 92cb749e16 Fix tests for renamed fixtures and new stacked notifications
Clear assignee's existing notifications in setup since Notifier now
uses create_or_find_by instead of create, and reload the association
to avoid caching after destroy_all.

Update fixture references after rebase: use `logo_assignment_kevin`
as base notification and `logo_mentioned_david` for mention tests.
Update source to `logo_published` in tests that need generic card events.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 19:31:13 +01:00
Rosa Gutierrez 126ccb5e3a Add integration test for notification delivery and simplify test helpers
- Add comprehensive integration test covering card assignment, comments,
  mentions, email bundling, and edge cases (system user, inactive user)
- Inline push notification test helpers into the only test that uses them
- Remove separate PushNotificationTestHelper module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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 0948533da7 Rename push to process on PushTarget for clearer semantics
"Push to target" reads naturally - we push the notification to the
target. "Target processes" also makes sense - the target receives
and handles the notification in its own way.

- Add class method PushTarget.process(notification) that instantiates
  and calls the instance method
- Rename instance method from push to process
- Add private push_to helper in Pushable for readable iteration

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
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 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
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
Jorge Manrubia c56dbb5f1e Add upgrade/downgrade options 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 b39a9ecab4 Make active_subscription a private method 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 984a5dd4ce Add Stripe-based billing system
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Jason Zimdars <jz@37signals.com>
2025-12-16 12:00:23 +01:00
Mike Dalessio d82d68fcd0 Restrict logins to employees in staging
as inferred from the email domain, which must be "@37signals.com" or
"@basecamp.com".
2025-12-12 15:07:16 -05:00
Jorge Manrubia 7298d2534a Improve test 2025-11-27 13:24:43 +01:00
Jorge Manrubia 1217f569ad This is a number, no need to convert 2025-11-27 13:20:25 +01:00
Jorge Manrubia 3d849a202a Move signup code back to the app, leave only queenbee hooks 2025-11-27 12:52:12 +01:00
Jorge Manrubia ebc212c1d7 Fix: the queenbe mocks were not really being applied 2025-11-24 16:26:41 +01:00
Jorge Manrubia 8019aa154f Stub QB requests in tests 2025-11-24 15:59:09 +01:00
Jorge Manrubia 0a4590a841 Fix problems referencing host model classes 2025-11-23 06:48:00 +01:00
Jorge Manrubia db57a7ba0f Initial commit: Move fizzy-saas to separate repository 2025-11-23 06:04:20 +01:00