Skip to content

Commit

Permalink
Add Stripe Checkout and Customer Billing Portal support (#7)
Browse files Browse the repository at this point in the history
This PR aims to close #6.

In addition to Stripe Checkout and Customer Billing Portal support, it's worth noting that I also added the ability to show a List of LineItems with each invoice. This was helpful for processing `invoice.paid` webhook events.
  • Loading branch information
stephendolan authored Dec 28, 2020
1 parent 39682f5 commit 97cbdc0
Show file tree
Hide file tree
Showing 13 changed files with 366 additions and 0 deletions.
13 changes: 13 additions & 0 deletions spec/stripe/methods/billing_portal_session_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require "../../spec_helper"

describe Stripe::BillingPortal::Session do
it "create billing portal session" do
WebMock.stub(:post, "https://api.stripe.com/v1/billing_portal/sessions")
.to_return(status: 200, body: File.read("spec/support/create_billing_portal_session.json"), headers: {"Content-Type" => "application/json"})

billing_portal_session = Stripe::BillingPortal::Session.create(customer: "cus_Ie0yEXsGjNVKMT")

billing_portal_session.id.should eq("bps_1I31kfBIZW9yg15I3FuLNDpj")
billing_portal_session.url.should_not be_nil
end
end
27 changes: 27 additions & 0 deletions spec/stripe/methods/checkout_session_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require "../../spec_helper"

describe Stripe::Checkout::Session do
it "create checkout session" do
WebMock.stub(:post, "https://api.stripe.com/v1/checkout/sessions")
.to_return(status: 200, body: File.read("spec/support/create_checkout_session.json"), headers: {"Content-Type" => "application/json"})

checkout_session = Stripe::Checkout::Session.create(mode: "payment", payment_method_types: ["card"], cancel_url: "https://test.com", success_url: "https://test.com", line_items: [{quantity: 1, price: "price_1234awioejawef"}])
checkout_session.id.should eq("cs_test_UlX36aKH1SdPk4Mtwp9z5S3Lkr7aIhbFbyQaCWev0aqX7mdmElg32LGr")
end

it "retrieve checkout session" do
WebMock.stub(:get, "https://api.stripe.com/v1/checkout/sessions/asddad")
.to_return(status: 200, body: File.read("spec/support/retrieve_checkout_session.json"), headers: {"Content-Type" => "application/json"})

checkout_session = Stripe::Checkout::Session.retrieve("asddad")
checkout_session.id.should eq("cs_test_Kkhdy5G3NRigIiMZr198DxpXUxirwrM1xFqGEt5zk5PH3AzDy7krza7g")
end

it "listing checkout sessions" do
WebMock.stub(:get, "https://api.stripe.com/v1/checkout/sessions")
.to_return(status: 200, body: File.read("spec/support/list_checkout_sessions.json"), headers: {"Content-Type" => "application/json"})

checkout_sessions = Stripe::Checkout::Session.list
checkout_sessions.first.id.should eq("cs_test_Kkhdy5G3NRigIiMZr198DxpXUxirwrM1xFqGEt5zk5PH3AzDy7krza7g")
end
end
9 changes: 9 additions & 0 deletions spec/support/create_billing_portal_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "bps_1I31kfBIZW9yg15I3FuLNDpj",
"object": "billing_portal.session",
"created": 1609085149,
"customer": "cus_Ie0yEXsGjNVKMT",
"livemode": true,
"return_url": "https://example.com/account",
"url": "https://billing.stripe.com/session/{SESSION_SECRET}"
}
35 changes: 35 additions & 0 deletions spec/support/create_checkout_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"id": "cs_test_UlX36aKH1SdPk4Mtwp9z5S3Lkr7aIhbFbyQaCWev0aqX7mdmElg32LGr",
"object": "checkout.session",
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"billing_address_collection": null,
"cancel_url": "https://example.com/cancel",
"client_reference_id": null,
"currency": null,
"customer": null,
"customer_email": null,
"livemode": false,
"locale": null,
"metadata": {},
"mode": "payment",
"payment_intent": "pi_1DjL5b2eZvKYlo2CPRMUEHyq",
"payment_method_types": [
"card"
],
"payment_status": "unpaid",
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"submit_type": null,
"subscription": null,
"success_url": "https://example.com/success",
"total_details": null,
"line_items": [
{
"price": "price_H5ggYwtDq4fbrJ",
"quantity": 2
}
]
}
61 changes: 61 additions & 0 deletions spec/support/list_checkout_sessions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"object": "list",
"url": "/v1/checkout/sessions",
"has_more": false,
"data": [
{
"id": "cs_test_Kkhdy5G3NRigIiMZr198DxpXUxirwrM1xFqGEt5zk5PH3AzDy7krza7g",
"object": "checkout.session",
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"billing_address_collection": null,
"cancel_url": "https://example.com/cancel",
"client_reference_id": null,
"currency": null,
"customer": null,
"customer_email": null,
"livemode": false,
"locale": null,
"metadata": {},
"mode": "payment",
"payment_intent": "pi_1DjL5b2eZvKYlo2CPRMUEHyq",
"payment_method_types": ["card"],
"payment_status": "unpaid",
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"submit_type": null,
"subscription": null,
"success_url": "https://example.com/success",
"total_details": null
},
{
"id": "cs_test_Kkhdy5G3NRigIiMZr198DxpXUxirwrM1xFqGEt5zk5PH3AzDy7krza7x",
"object": "checkout.session",
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"billing_address_collection": null,
"cancel_url": "https://example.com/cancel",
"client_reference_id": null,
"currency": null,
"customer": null,
"customer_email": null,
"livemode": false,
"locale": null,
"metadata": {},
"mode": "payment",
"payment_intent": "pi_1DjL5b2eZvKYlo2CPxMUEHyq",
"payment_method_types": ["card"],
"payment_status": "unpaid",
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"submit_type": null,
"subscription": null,
"success_url": "https://example.com/success",
"total_details": null
}
]
}
29 changes: 29 additions & 0 deletions spec/support/retrieve_checkout_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"id": "cs_test_Kkhdy5G3NRigIiMZr198DxpXUxirwrM1xFqGEt5zk5PH3AzDy7krza7g",
"object": "checkout.session",
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"billing_address_collection": null,
"cancel_url": "https://example.com/cancel",
"client_reference_id": null,
"currency": null,
"customer": null,
"customer_email": null,
"livemode": false,
"locale": null,
"metadata": {},
"mode": "payment",
"payment_intent": "pi_1DjL5b2eZvKYlo2CPRMUEHyq",
"payment_method_types": [
"card"
],
"payment_status": "unpaid",
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"submit_type": null,
"subscription": null,
"success_url": "https://example.com/success",
"total_details": null
}
25 changes: 25 additions & 0 deletions src/stripe/methods/core/billing_portal/sessions/create_session.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# https://stripe.com/docs/api/customer_portal/create
class Stripe::BillingPortal::Session
def self.create(
customer : String | Stripe::Customer,
return_url : String? = nil,
expand : Array(String)? = nil
) : Stripe::BillingPortal::Session
customer = customer.id if customer.is_a?(Customer)

io = IO::Memory.new
builder = ParamsBuilder.new(io)

{% for x in %w(customer return_url expand) %}
builder.add({{x}}, {{x.id}}) unless {{x.id}}.nil?
{% end %}

response = Stripe.client.post("/v1/billing_portal/sessions", form: io.to_s)

if response.status_code == 200
return Stripe::BillingPortal::Session.from_json(response.body)
else
raise Error.from_json(response.body, "error")
end
end
end
31 changes: 31 additions & 0 deletions src/stripe/methods/core/checkout/sessions/create_session.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# https://stripe.com/docs/api/checkout/sessions/create
class Stripe::Checkout::Session
def self.create(
mode : String | Stripe::Checkout::Session::Mode,
payment_method_types : Array(String),
cancel_url : String,
success_url : String,
client_reference_id : String? = nil,
customer : String? | Stripe::Customer? = nil,
customer_email : String? = nil,
line_items : Array(NamedTuple(quantity: Int32, price: String))? = nil,
expand : Array(String)? = nil
) : Stripe::Checkout::Session
customer = customer.not_nil!.id if customer.is_a?(Customer)

io = IO::Memory.new
builder = ParamsBuilder.new(io)

{% for x in %w(mode payment_method_types cancel_url success_url client_reference_id customer customer_email line_items expand) %}
builder.add({{x}}, {{x.id}}) unless {{x.id}}.nil?
{% end %}

response = Stripe.client.post("/v1/checkout/sessions", form: io.to_s)

if response.status_code == 200
return Stripe::Checkout::Session.from_json(response.body)
else
raise Error.from_json(response.body, "error")
end
end
end
25 changes: 25 additions & 0 deletions src/stripe/methods/core/checkout/sessions/list_sessions.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# https://stripe.com/docs/api/checkout/sessions/list
class Stripe::Checkout::Session
def self.list(
payment_intent : String? | Stripe::PaymentIntent? = nil,
subscription : String? | Stripe::Subscription? = nil,
limit : Int32? = nil,
starting_after : String? = nil,
ending_before : String? = nil
) : List(Stripe::Checkout::Session)
io = IO::Memory.new
builder = ParamsBuilder.new(io)

{% for x in %w(payment_intent subscription limit starting_after ending_before) %}
builder.add({{x}}, {{x.id}}) unless {{x.id}}.nil?
{% end %}

response = Stripe.client.get("/v1/checkout/sessions", form: io.to_s)

if response.status_code == 200
return List(Stripe::Checkout::Session).from_json(response.body)
else
raise Error.from_json(response.body, "error")
end
end
end
16 changes: 16 additions & 0 deletions src/stripe/methods/core/checkout/sessions/retrieve_session.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# https://stripe.com/docs/api/checkout/sessions/retrieve
class Stripe::Checkout::Session
def self.retrieve(id : String)
response = Stripe.client.get("/v1/checkout/sessions/#{id}")

if response.status_code == 200
return Stripe::Checkout::Session.from_json(response.body)
else
raise Error.from_json(response.body, "error")
end
end

def self.retrieve(session : Session)
retrieve(session.id)
end
end
18 changes: 18 additions & 0 deletions src/stripe/objects/core/billing_portal/session.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# https://stripe.com/docs/api/customer_portal
class Stripe::BillingPortal::Session
include JSON::Serializable

getter id : String
getter object : String? = "billing_portal.session"

getter return_url : String

getter customer : String? | Stripe::Customer?

@[JSON::Field(converter: Time::EpochConverter)]
getter created : Time?

getter livemode : Bool?

getter url : String?
end
75 changes: 75 additions & 0 deletions src/stripe/objects/core/checkout/session.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# https://stripe.com/docs/api/checkout/sessions/object
class Stripe::Checkout::Session
include JSON::Serializable

enum Mode
Payment
Setup
Subscription
end

enum PaymentStatus
Paid
Unpaid
NoPaymentRequired
end

enum BillingAddressCollection
Auto
Required
end

enum SubmitType
Auto
Pay
Book
Donate
end

getter id : String
getter object : String? = "checkout.session"

@[JSON::Field(converter: Enum::StringConverter(Stripe::Checkout::Session::Mode))]
getter mode : String | Mode
getter payment_method_types : Array(String)

getter cancel_url : String
getter success_url : String

getter client_reference_id : String?
getter customer : String? | Stripe::Customer?
getter customer_email : String?

getter line_items : Array(Hash(String, String | Int32))?

getter metadata : Hash(String, String)?

getter payment_intent : String? | Stripe::PaymentIntent?
@[JSON::Field(converter: Enum::StringConverter(Stripe::Checkout::Session::PaymentStatus))]
getter payment_status : PaymentStatus

getter subscription : String? | Stripe::Subscription?

getter allow_promotion_codes : Bool?

getter amount_subtotal : Int32?
getter amount_total : Int32?

@[JSON::Field(converter: Enum::StringConverter(Stripe::Checkout::Session::BillingAddressCollection))]
getter billing_address_collection : BillingAddressCollection?

getter currency : String?

getter livemode : Bool?

getter setup_intent : String? | Stripe::SetupIntent?

getter shipping : Hash(String, String | Hash(String, String))?

getter shipping_address_collection : Hash(String, Array(String))?

@[JSON::Field(converter: Enum::StringConverter(Stripe::Checkout::Session::SubmitType))]
getter submit_type : SubmitType?

getter subscription : String? | Stripe::Subscription?
end
2 changes: 2 additions & 0 deletions src/stripe/objects/core/invoice.cr
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class Stripe::Invoice

getter ending_balance : Int32?

getter lines : List(LineItem)?

@[JSON::Field(converter: Time::EpochConverter)]
getter due_date : Time?
getter invoice_pdf : String?
Expand Down

0 comments on commit 97cbdc0

Please sign in to comment.