payflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/app/controllers/payflow/application_controller.rb +6 -0
- data/app/controllers/payflow/webhooks/asaas_controller.rb +18 -0
- data/app/controllers/payflow/webhooks/stripe_controller.rb +18 -0
- data/app/jobs/payflow/application_job.rb +6 -0
- data/app/jobs/payflow/overdue_accounts_job.rb +16 -0
- data/app/jobs/payflow/webhook_job.rb +21 -0
- data/app/models/payflow/application_record.rb +7 -0
- data/app/models/payflow/invoice.rb +18 -0
- data/app/models/payflow/subscription.rb +32 -0
- data/app/models/payflow/webhook_event.rb +19 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20260617180001_create_payflow_subscriptions.rb +19 -0
- data/db/migrate/20260617180002_create_payflow_invoices.rb +18 -0
- data/db/migrate/20260617180003_create_payflow_webhook_events.rb +17 -0
- data/lib/generators/payflow/install_generator.rb +40 -0
- data/lib/generators/payflow/templates/initializer.rb +13 -0
- data/lib/generators/payflow/templates/migration_invoice.rb +16 -0
- data/lib/generators/payflow/templates/migration_subscription.rb +17 -0
- data/lib/generators/payflow/templates/migration_webhook_event.rb +15 -0
- data/lib/payflow/billable.rb +34 -0
- data/lib/payflow/configuration.rb +31 -0
- data/lib/payflow/engine.rb +23 -0
- data/lib/payflow/errors.rb +13 -0
- data/lib/payflow/events.rb +9 -0
- data/lib/payflow/provider_resolver.rb +18 -0
- data/lib/payflow/providers/asaas/customer.rb +17 -0
- data/lib/payflow/providers/asaas/provider.rb +41 -0
- data/lib/payflow/providers/asaas/subscription.rb +50 -0
- data/lib/payflow/providers/asaas/webhook.rb +43 -0
- data/lib/payflow/providers/base.rb +51 -0
- data/lib/payflow/providers/stripe/provider.rb +40 -0
- data/lib/payflow/providers/stripe/subscription.rb +28 -0
- data/lib/payflow/providers/stripe/webhook.rb +31 -0
- data/lib/payflow/subscription_service.rb +48 -0
- data/lib/payflow/version.rb +5 -0
- data/lib/payflow/webhooks/dispatcher.rb +50 -0
- data/lib/payflow/webhooks/signature_verifier.rb +57 -0
- data/lib/payflow.rb +39 -0
- data/lib/tasks/payflow_tasks.rake +28 -0
- metadata +184 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 97e6993b930178e108e6b1b805433af2216373def14db6abc037f44eab831b7d
|
|
4
|
+
data.tar.gz: 2de44cf55db703c4da5475a09d4bb872f35ac3745fe4bedf82b6813b5a90bb53
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3392f52cb1c7e66a99da3d003987e9750070dd6b8653ee807a74f277f55570d56a6e2f35504646b62df6d13f38e97e53296fb21539f60499c8c80207a81c4b88
|
|
7
|
+
data.tar.gz: 7d071c31cb70af1b1157c49c87c208ffab0f10ae2ad23563859eb20a87e8440e8f47af56517b605e5bc267ad4b63fc2e61202be0194519c88ba121aaacd90dc7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Conexus Systems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Payflow
|
|
2
|
+
|
|
3
|
+
Unified billing engine for Rails applications. Payflow abstracts subscription management and payment webhooks behind a provider-agnostic API.
|
|
4
|
+
|
|
5
|
+
**MVP 0.1.0 providers:** Asaas (functional), Stripe (basic).
|
|
6
|
+
|
|
7
|
+
Mercado Pago and Pagar.me are on the [roadmap](ROADMAP.md) — not included in this release.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "payflow"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
rails generate payflow:install
|
|
22
|
+
rails db:migrate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Alternatively, copy engine migrations without the generator:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
rails payflow:install:migrations
|
|
29
|
+
rails db:migrate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
Create `config/initializers/payflow.rb` (or use the generator output):
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Payflow.configure do |config|
|
|
38
|
+
config.provider = :asaas
|
|
39
|
+
|
|
40
|
+
config.asaas_api_key = ENV["ASAAS_API_KEY"]
|
|
41
|
+
config.asaas_webhook_token = ENV["ASAAS_WEBHOOK_TOKEN"]
|
|
42
|
+
|
|
43
|
+
config.stripe_api_key = ENV["STRIPE_API_KEY"]
|
|
44
|
+
config.stripe_webhook_secret = ENV["STRIPE_WEBHOOK_SECRET"]
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Mount the engine in `config/routes.rb`:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
mount Payflow::Engine => "/payflow"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
Include `Payflow::Billable` in your host model:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class Organization < ApplicationRecord
|
|
60
|
+
include Payflow::Billable
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Subscribe and manage billing:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
organization.subscribe!(plan: "premium", provider: Payflow.config.provider)
|
|
68
|
+
organization.cancel_subscription!
|
|
69
|
+
organization.active_subscription?
|
|
70
|
+
organization.can_access_system? # active and not overdue
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Webhooks
|
|
74
|
+
|
|
75
|
+
Configure your payment providers to send webhooks to:
|
|
76
|
+
|
|
77
|
+
| Provider | Endpoint |
|
|
78
|
+
|----------|----------|
|
|
79
|
+
| Asaas | `POST /payflow/webhooks/asaas` |
|
|
80
|
+
| Stripe | `POST /payflow/webhooks/stripe` |
|
|
81
|
+
|
|
82
|
+
Webhooks are verified, persisted as `Payflow::WebhookEvent` records, and processed asynchronously via `Payflow::WebhookJob` and Sidekiq (`:payflow` queue).
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Host App
|
|
88
|
+
└── Payflow::Engine (mounted at /payflow)
|
|
89
|
+
├── Payflow::Billable concern
|
|
90
|
+
├── Models: Subscription, Invoice, WebhookEvent
|
|
91
|
+
├── Jobs: WebhookJob, OverdueAccountsJob
|
|
92
|
+
├── Payflow::ProviderResolver
|
|
93
|
+
│ ├── Providers::Asaas::Provider
|
|
94
|
+
│ └── Providers::Stripe::Provider
|
|
95
|
+
└── Webhooks::Dispatcher
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- **Billable** — polymorphic `billable` association on subscriptions
|
|
99
|
+
- **ProviderResolver** — `Payflow::ProviderResolver.for(:asaas)` returns the provider client
|
|
100
|
+
- **WebhookJob** — receives `provider` + `payload`, dispatches to update subscription/invoice state
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bundle install
|
|
106
|
+
bundle exec rspec
|
|
107
|
+
bundle exec rubocop
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Releasing
|
|
111
|
+
|
|
112
|
+
See [RELEASE.md](RELEASE.md) for the RubyGems publishing checklist.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT — Copyright (c) Conexus Systems. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
module Webhooks
|
|
5
|
+
class AsaasController < ApplicationController
|
|
6
|
+
def receive
|
|
7
|
+
verifier = SignatureVerifier.new(provider: :asaas, request: request)
|
|
8
|
+
return head :unauthorized unless verifier.valid?
|
|
9
|
+
|
|
10
|
+
payload = JSON.parse(request.raw_post)
|
|
11
|
+
WebhookJob.perform_later(provider: :asaas, payload: payload)
|
|
12
|
+
head :ok
|
|
13
|
+
rescue JSON::ParserError
|
|
14
|
+
head :bad_request
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
module Webhooks
|
|
5
|
+
class StripeController < ApplicationController
|
|
6
|
+
def receive
|
|
7
|
+
verifier = SignatureVerifier.new(provider: :stripe, request: request)
|
|
8
|
+
return head :unauthorized unless verifier.valid?
|
|
9
|
+
|
|
10
|
+
payload = JSON.parse(request.raw_post)
|
|
11
|
+
WebhookJob.perform_later(provider: :stripe, payload: payload)
|
|
12
|
+
head :ok
|
|
13
|
+
rescue JSON::ParserError
|
|
14
|
+
head :bad_request
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class OverdueAccountsJob < ApplicationJob
|
|
5
|
+
queue_as :payflow
|
|
6
|
+
|
|
7
|
+
def perform
|
|
8
|
+
Subscription.active.find_each do |subscription|
|
|
9
|
+
latest_invoice = subscription.invoices.order(created_at: :desc).first
|
|
10
|
+
next unless latest_invoice&.overdue?
|
|
11
|
+
|
|
12
|
+
subscription.update!(status: :overdue)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class WebhookJob < ApplicationJob
|
|
5
|
+
queue_as :payflow
|
|
6
|
+
|
|
7
|
+
def perform(provider:, payload:)
|
|
8
|
+
event = WebhookEvent.create!(
|
|
9
|
+
provider: provider.to_s,
|
|
10
|
+
payload: payload,
|
|
11
|
+
status: :pending
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
Webhooks::Dispatcher.new(webhook_event: event).dispatch!
|
|
15
|
+
event.update!(status: :processed, processed_at: Time.current)
|
|
16
|
+
rescue StandardError
|
|
17
|
+
event&.update!(status: :failed)
|
|
18
|
+
raise
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class Invoice < ApplicationRecord
|
|
5
|
+
self.table_name = "payflow_invoices"
|
|
6
|
+
|
|
7
|
+
belongs_to :subscription, class_name: "Payflow::Subscription"
|
|
8
|
+
|
|
9
|
+
enum :status, {
|
|
10
|
+
pending: "pending",
|
|
11
|
+
paid: "paid",
|
|
12
|
+
overdue: "overdue",
|
|
13
|
+
cancelled: "cancelled"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
validates :external_id, presence: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class Subscription < ApplicationRecord
|
|
5
|
+
self.table_name = "payflow_subscriptions"
|
|
6
|
+
|
|
7
|
+
belongs_to :billable, polymorphic: true
|
|
8
|
+
has_many :invoices, class_name: "Payflow::Invoice", dependent: :destroy
|
|
9
|
+
|
|
10
|
+
enum :status, {
|
|
11
|
+
pending: "pending",
|
|
12
|
+
active: "active",
|
|
13
|
+
overdue: "overdue",
|
|
14
|
+
cancelled: "cancelled",
|
|
15
|
+
expired: "expired"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
validates :plan, :provider, :external_id, presence: true
|
|
19
|
+
|
|
20
|
+
def active?
|
|
21
|
+
status == "active"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def overdue?
|
|
25
|
+
status == "overdue"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cancelled?
|
|
29
|
+
status == "cancelled"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class WebhookEvent < ApplicationRecord
|
|
5
|
+
self.table_name = "payflow_webhook_events"
|
|
6
|
+
|
|
7
|
+
enum :status, {
|
|
8
|
+
pending: "pending",
|
|
9
|
+
processed: "processed",
|
|
10
|
+
failed: "failed"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
validates :provider, :payload, presence: true
|
|
14
|
+
|
|
15
|
+
def processed?
|
|
16
|
+
status == "processed"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePayflowSubscriptions < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :payflow_subscriptions do |t|
|
|
6
|
+
t.references :billable, polymorphic: true, null: false, index: true
|
|
7
|
+
t.string :plan, null: false
|
|
8
|
+
t.string :provider, null: false
|
|
9
|
+
t.string :external_id, null: false
|
|
10
|
+
t.string :status, null: false, default: "pending"
|
|
11
|
+
t.datetime :cancelled_at
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :payflow_subscriptions, :external_id, unique: true
|
|
17
|
+
add_index :payflow_subscriptions, :status
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePayflowInvoices < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :payflow_invoices do |t|
|
|
6
|
+
t.references :subscription, null: false, foreign_key: { to_table: :payflow_subscriptions }
|
|
7
|
+
t.string :external_id, null: false
|
|
8
|
+
t.string :status, null: false, default: "pending"
|
|
9
|
+
t.decimal :amount, precision: 10, scale: 2
|
|
10
|
+
t.datetime :due_date
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_index :payflow_invoices, :external_id, unique: true
|
|
16
|
+
add_index :payflow_invoices, :status
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePayflowWebhookEvents < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :payflow_webhook_events do |t|
|
|
6
|
+
t.string :provider, null: false
|
|
7
|
+
t.json :payload, null: false, default: {}
|
|
8
|
+
t.string :status, null: false, default: "pending"
|
|
9
|
+
t.datetime :processed_at
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :payflow_webhook_events, :status
|
|
15
|
+
add_index :payflow_webhook_events, :provider
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module Payflow
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Creates Payflow initializer and database migrations"
|
|
14
|
+
|
|
15
|
+
def create_initializer
|
|
16
|
+
template "initializer.rb", "config/initializers/payflow.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_migrations
|
|
20
|
+
migration_template "migration_subscription.rb", "db/migrate/create_payflow_subscriptions.rb"
|
|
21
|
+
migration_template "migration_invoice.rb", "db/migrate/create_payflow_invoices.rb"
|
|
22
|
+
migration_template "migration_webhook_event.rb", "db/migrate/create_payflow_webhook_events.rb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def show_readme
|
|
26
|
+
return unless behavior == :invoke
|
|
27
|
+
|
|
28
|
+
say "\nPayflow installed successfully!", :green
|
|
29
|
+
say " Run `rails db:migrate` to apply the migrations."
|
|
30
|
+
say ""
|
|
31
|
+
say "Note: Payflow::Engine also appends engine migrations when the gem is loaded."
|
|
32
|
+
say "Use either this generator OR `rails payflow:install:migrations` — not both."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.next_migration_number(_dirname)
|
|
36
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Payflow.configure do |config|
|
|
4
|
+
config.default_provider = :asaas
|
|
5
|
+
|
|
6
|
+
config.asaas_api_key = ENV.fetch("ASAAS_API_KEY", nil)
|
|
7
|
+
config.asaas_webhook_token = ENV.fetch("ASAAS_WEBHOOK_TOKEN", nil)
|
|
8
|
+
|
|
9
|
+
config.stripe_api_key = ENV.fetch("STRIPE_API_KEY", nil)
|
|
10
|
+
config.stripe_webhook_secret = ENV.fetch("STRIPE_WEBHOOK_SECRET", nil)
|
|
11
|
+
|
|
12
|
+
# config.billable_class_name = "Organization"
|
|
13
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreatePayflowInvoices < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :payflow_invoices do |t|
|
|
4
|
+
t.references :subscription, null: false, foreign_key: { to_table: :payflow_subscriptions }
|
|
5
|
+
t.string :external_id, null: false
|
|
6
|
+
t.string :status, null: false, default: "pending"
|
|
7
|
+
t.decimal :amount, precision: 10, scale: 2
|
|
8
|
+
t.datetime :due_date
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :payflow_invoices, :external_id, unique: true
|
|
14
|
+
add_index :payflow_invoices, :status
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreatePayflowSubscriptions < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :payflow_subscriptions do |t|
|
|
4
|
+
t.references :billable, polymorphic: true, null: false, index: true
|
|
5
|
+
t.string :plan, null: false
|
|
6
|
+
t.string :provider, null: false
|
|
7
|
+
t.string :external_id, null: false
|
|
8
|
+
t.string :status, null: false, default: "pending"
|
|
9
|
+
t.datetime :cancelled_at
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :payflow_subscriptions, :external_id, unique: true
|
|
15
|
+
add_index :payflow_subscriptions, :status
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreatePayflowWebhookEvents < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :payflow_webhook_events do |t|
|
|
4
|
+
t.string :provider, null: false
|
|
5
|
+
t.json :payload, null: false, default: {}
|
|
6
|
+
t.string :status, null: false, default: "pending"
|
|
7
|
+
t.datetime :processed_at
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :payflow_webhook_events, :status
|
|
13
|
+
add_index :payflow_webhook_events, :provider
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
module Billable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
has_many :payflow_subscriptions,
|
|
9
|
+
as: :billable,
|
|
10
|
+
class_name: "Payflow::Subscription",
|
|
11
|
+
dependent: :destroy
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def subscribe!(plan:, provider: Payflow.config.provider)
|
|
15
|
+
SubscriptionService.new(billable: self).subscribe!(plan: plan, provider: provider)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def cancel_subscription!
|
|
19
|
+
SubscriptionService.new(billable: self).cancel!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def subscription
|
|
23
|
+
payflow_subscriptions.order(created_at: :desc).first
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def active_subscription?
|
|
27
|
+
subscription&.active?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def can_access_system?
|
|
31
|
+
active_subscription? && !subscription&.overdue?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :provider,
|
|
6
|
+
:asaas_api_key,
|
|
7
|
+
:asaas_webhook_token,
|
|
8
|
+
:stripe_api_key,
|
|
9
|
+
:stripe_webhook_secret,
|
|
10
|
+
:billable_class_name
|
|
11
|
+
|
|
12
|
+
alias default_provider provider
|
|
13
|
+
alias default_provider= provider=
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@provider = :asaas
|
|
17
|
+
@billable_class_name = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def provider_credentials(provider_name)
|
|
21
|
+
case provider_name.to_sym
|
|
22
|
+
when :asaas
|
|
23
|
+
{ api_key: asaas_api_key, webhook_token: asaas_webhook_token }
|
|
24
|
+
when :stripe
|
|
25
|
+
{ api_key: stripe_api_key, webhook_secret: stripe_webhook_secret }
|
|
26
|
+
else
|
|
27
|
+
raise ProviderNotFoundError, "Unknown provider: #{provider_name}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Payflow
|
|
6
|
+
|
|
7
|
+
config.generators do |generator|
|
|
8
|
+
generator.test_framework :rspec
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "payflow.migrations" do |app|
|
|
12
|
+
unless app.root.to_s.match?(root.to_s)
|
|
13
|
+
config.paths["db/migrate"].expanded.each do |path|
|
|
14
|
+
app.config.paths["db/migrate"] << path
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
rake_tasks do
|
|
20
|
+
load root.join("lib/tasks/payflow_tasks.rake")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
class ProviderError < Error; end
|
|
8
|
+
class ProviderNotFoundError < ProviderError; end
|
|
9
|
+
class InvalidWebhookError < Error; end
|
|
10
|
+
class SignatureVerificationError < InvalidWebhookError; end
|
|
11
|
+
class RecordNotFoundError < Error; end
|
|
12
|
+
class SubscriptionError < Error; end
|
|
13
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
class ProviderResolver
|
|
5
|
+
REGISTRY = {
|
|
6
|
+
asaas: Providers::Asaas::Provider,
|
|
7
|
+
stripe: Providers::Stripe::Provider
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def self.for(name)
|
|
11
|
+
key = name.to_sym
|
|
12
|
+
klass = REGISTRY[key]
|
|
13
|
+
raise ProviderNotFoundError, "Unknown provider: #{key}" unless klass
|
|
14
|
+
|
|
15
|
+
klass.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payflow
|
|
4
|
+
module Providers
|
|
5
|
+
module Asaas
|
|
6
|
+
class Customer
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create(attrs)
|
|
12
|
+
{ id: "cus_asaas_stub_#{SecureRandom.hex(4)}", provider: :asaas, email: attrs[:email], name: attrs[:name] }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|