From a43f0d309e16d8354e3dddb182362e0ea5fca9f6 Mon Sep 17 00:00:00 2001 From: ntr Date: Wed, 19 Jun 2019 22:04:25 +1000 Subject: [PATCH] sessions parsing --- WORKLOG.md | 11 -- client/assets/styles/styles.css | 1 + client/src/components/account.status.jsx | 21 ++- server/Cargo.toml | 9 +- server/src/main.rs | 3 +- server/src/mtx.rs | 114 ++++++++++++++- server/src/net.rs | 29 ++-- .../checkout.session.completed.purchase.json | 64 +++++++++ ...eckout.session.completed.subscription.json | 63 ++++++++ .../test/checkout.session.completed.test.json | 43 ++++++ .../test/customer.subscription.created.json | 109 ++++++++++++++ .../test/customer.subscription.updated.json | 134 ++++++++++++++++++ server/test/subscription.json | 101 ------------- 13 files changed, 568 insertions(+), 134 deletions(-) create mode 100644 server/test/checkout.session.completed.purchase.json create mode 100644 server/test/checkout.session.completed.subscription.json create mode 100644 server/test/checkout.session.completed.test.json create mode 100644 server/test/customer.subscription.created.json create mode 100644 server/test/customer.subscription.updated.json delete mode 100644 server/test/subscription.json diff --git a/WORKLOG.md b/WORKLOG.md index 20cb28d1..85cc2537 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -29,17 +29,6 @@ *$$$* -clicks buy - creates stripe order / fill 0x order - -server notified of payment - txs <- payment - - -buy supporter pack - account credited with features - char sets - emotes * balances table (ingame currency) diff --git a/client/assets/styles/styles.css b/client/assets/styles/styles.css index 63dd89a3..eb18d3ef 100644 --- a/client/assets/styles/styles.css +++ b/client/assets/styles/styles.css @@ -465,6 +465,7 @@ header { .stripe-btn { width: 100%; padding: 0 0.5em; + margin: 0.25em 0; background: whitesmoke; color: black; border-radius: 2px; diff --git a/client/src/components/account.status.jsx b/client/src/components/account.status.jsx index 5fbe9282..2a7d6edf 100644 --- a/client/src/components/account.status.jsx +++ b/client/src/components/account.status.jsx @@ -15,7 +15,7 @@ function BitsBtn(args) { stripe, account, } = args; - function onClick(e) { + function subscribeClick(e) { stripe.redirectToCheckout({ items: [{plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1}], successUrl: 'http://localhost:40080/payments/success', @@ -23,16 +23,31 @@ function BitsBtn(args) { clientReferenceId: account.id }); } + + function bitsClick(e) { + stripe.redirectToCheckout({ + items: [{sku: 'sku_FHUfNEhWQaVDaT', quantity: 1}], + successUrl: 'http://localhost:40080/payments/success', + cancelUrl: 'http://localhost:40080/payments/cancel', + clientReferenceId: account.id + }); + } return (
+ +
); } diff --git a/server/Cargo.toml b/server/Cargo.toml index 2f62374a..6610612d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -9,6 +9,7 @@ uuid = { version = "0.5", features = ["serde", "v4"] } serde = "1" serde_derive = "1" serde_cbor = "0.9" +serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } @@ -28,7 +29,9 @@ fern = "0.5" actix = "0.8.2" actix-web = "1.0.0" actix-web-actors = "1.0.0" -futures = "0.1" -bytes = "0.4" +actix-cors = "0.1.0" -stripe-rust = "0.10.2" +stripe-rust = { version = "0.10", features = ["webhooks"] } + +[patch.crates-io] +stripe-rust = { path = "/home/ntr/code/stripe-rs" } diff --git a/server/src/main.rs b/server/src/main.rs index 94f3df8e..42cbe65a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -10,6 +10,7 @@ extern crate r2d2_postgres; extern crate fallible_iterator; extern crate actix; +extern crate actix_cors; extern crate actix_web; extern crate actix_web_actors; @@ -58,7 +59,7 @@ fn setup_logger() -> Result<(), fern::InitError> { )) }) .level_for("postgres", log::LevelFilter::Info) - .level(log::LevelFilter::Info) + .level(log::LevelFilter::Debug) .chain(std::io::stdout()) .chain(fern::log_file("log/mnml.log")?) .apply()?; diff --git a/server/src/mtx.rs b/server/src/mtx.rs index 3d113323..bf84c101 100644 --- a/server/src/mtx.rs +++ b/server/src/mtx.rs @@ -1,15 +1,115 @@ +use uuid::Uuid; use actix_web::{web, HttpResponse}; -use stripe::{CheckoutSession}; +use actix::prelude::*; -use net::{State, MnmlError}; +use stripe::{Event, EventObject, CheckoutSession}; -pub fn stripe_payment(state: web::Data, body: web::Json::) -> Result { - let db = state.pool.get().or(Err(MnmlError::ServerError))?; - let mut tx = db.transaction().or(Err(MnmlError::ServerError))?; +use net::{State, PgPool, MnmlError}; - let session = body.into_inner(); +pub struct PaymentProcessor { + pool: PgPool, +} - info!("{:?}", session); +impl Actor for PaymentProcessor { + type Context = Context; + fn started(&mut self, _ctx: &mut Self::Context) { + info!("listening for PaymentProcessor"); + } +} + +impl Supervised for PaymentProcessor { + fn restarting(&mut self, _ctx: &mut Context) { + warn!("PaymentProcessor restarting"); + } +} + +impl PaymentProcessor { + pub fn new(pool: PgPool) -> PaymentProcessor { + PaymentProcessor { pool } + } +} + +impl Handler for PaymentProcessor { + type Result = Result, MnmlError>; + + fn handle(&mut self, msg: StripeEvent, _: &mut Context) -> Self::Result { + let event = msg.0; + match event.data.object { + EventObject::CheckoutSession(s) => process_stripe_checkout(s), + _ => Err(MnmlError::ServerError), + } + } +} + +fn process_stripe_checkout(session: CheckoutSession) -> Result, MnmlError> { + let account = match session.client_reference_id { + Some(a) => Uuid::parse_str(&a).or(Err(MnmlError::UnknownUser))?, + None => { + warn!("unknown user checkout {:?}", session); + return Err(MnmlError::UnknownUser) + }, + }; + + // checkout.session.completed + // assign stripe customer_id to account + + // if subscription + // go get it + // set account sub to active and end date + + // if just bits purchase + + + Ok(vec![]) +} + + +#[derive(Debug,Clone,Copy,Serialize,Deserialize)] +enum Mtx { + Subscription, + Currency, +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +struct Order { + account: Uuid, + customer: String, +} + +struct StripeEvent(Event); +impl Message for StripeEvent { + type Result = Result, MnmlError>; +} + +pub fn post_stripe_event(state: web::Data, body: web::Json::) -> Result { + let event: Event = body.into_inner(); + info!("stripe event {:?}", event); + state.payments.do_send(StripeEvent(event)); Ok(HttpResponse::Ok().finish()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::prelude::*; + use std::fs::File; + + #[test] + fn test_stripe_checkout() { + let mut f = File::open("./test/checkout.session.completed.purchase.json").expect("couldn't open file"); + let mut checkout_str = String::new(); + f.read_to_string(&mut checkout_str) + .expect("unable to read file"); + + let event: Event = serde_json::from_str(&checkout_str) + .expect("could not deserialize"); + + let mtx = match event.data.object { + EventObject::CheckoutSession(s) => process_stripe_checkout(s), + _ => panic!("unknown event obj"), + }; + + println!("got some fuckin bling {:?}", mtx); + } +} \ No newline at end of file diff --git a/server/src/net.rs b/server/src/net.rs index eafdd420..deb2775f 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -1,10 +1,10 @@ use std::env; use actix_web::{middleware, web, App, HttpMessage, HttpRequest, HttpResponse, HttpServer}; -use actix_web::middleware::cors::Cors; use actix_web::error::ResponseError; use actix_web::http::{Cookie}; use actix_web::cookie::{SameSite}; +use actix_cors::Cors; use actix::prelude::*; @@ -17,7 +17,7 @@ use warden::{Warden}; use pubsub::PubSub; use ws::{connect}; use account::{account_login, account_create, account_from_token, account_set_token}; -use mtx::{stripe_payment}; +use mtx::{PaymentProcessor, post_stripe_event}; pub type Db = PooledConnection; pub type PgPool = Pool; @@ -32,6 +32,9 @@ pub enum MnmlError { Unauthorized, #[fail(display="bad request")] BadRequest, + + #[fail(display="unknown user")] + UnknownUser, } impl ResponseError for MnmlError { @@ -51,6 +54,9 @@ impl ResponseError for MnmlError { .max_age(-1) // 1 week aligns with db set .finish()) .json(RpcErrorResponse { err: "unauthorized ".to_string() }), + + MnmlError::UnknownUser => HttpResponse::BadRequest() + .json(RpcErrorResponse { err: "unknown user".to_string() }), } } } @@ -139,6 +145,8 @@ fn create_pool(url: String) -> Pool { pub struct State { pub pool: PgPool, + pub payments: Addr, + pub pubsub: Addr, secure: bool, } @@ -147,23 +155,28 @@ pub fn start() { .expect("DATABASE_URL must be set"); let pool = create_pool(database_url); - let sys = System::new("mnml"); + let _sys = System::new("mnml"); - Warden::new(pool.clone()).start(); + let _warden = Warden::new(pool.clone()).start(); + + let payments = PaymentProcessor::new(pool.clone()).start(); let pubsub_conn = pool.get().expect("could not get pubsub pg connection"); - let pubsub_addr = Supervisor::start(move |_| PubSub::new(pubsub_conn)); + let pubsub = Supervisor::start(move |_| PubSub::new(pubsub_conn)); HttpServer::new(move || App::new() - .data(State { pool: pool.clone(), secure: false }) + .data(State { pool: pool.clone(), secure: false, payments: payments.clone(), pubsub: pubsub.clone() }) .wrap(middleware::Logger::default()) .wrap(Cors::new().supports_credentials()) .service(web::resource("/api/login").route(web::post().to(login))) .service(web::resource("/api/logout").route(web::post().to(logout))) .service(web::resource("/api/register").route(web::post().to(register))) - .service(web::resource("/api/payments/stripe").route(web::post().to(stripe_payment))) - .service(web::resource("/api/payments/crypto").route(web::post().to(stripe_payment))) + .service(web::resource("/api/payments/stripe") + .route(web::post().to(post_stripe_event))) + + // .service(web::resource("/api/payments/crypto") + // .route(web::post().to(post_stripe_payment))) .service(web::resource("/api/ws").route(web::get().to(connect)))) .bind("127.0.0.1:40000").expect("could not bind to port") diff --git a/server/test/checkout.session.completed.purchase.json b/server/test/checkout.session.completed.purchase.json new file mode 100644 index 00000000..70d7967c --- /dev/null +++ b/server/test/checkout.session.completed.purchase.json @@ -0,0 +1,64 @@ +{ + "id": "evt_1Emw4mGf8y65MteUNt6J2zlN", + "object": "event", + "api_version": "2019-05-16", + "created": 1560921076, + "data": { + "object": { + "id": "cs_test_0FldEAQpyiya1xFLtZpPuEaWQ8OGnMMsotlnwrWCk8jXrAeeMtVruHwB", + "object": "checkout.session", + "billing_address_collection": null, + "cancel_url": "http://localhost:40080/payments/cancel", + "client_reference_id": "ff3bbecb-e744-4674-b411-a11d6832a5ac", + "customer": "cus_FHV47hm01bNBpG", + "customer_email": null, + "display_items": [ + { + "amount": 500, + "currency": "aud", + "quantity": 1, + "sku": { + "id": "sku_FHUfNEhWQaVDaT", + "object": "sku", + "active": true, + "attributes": { + "name": "20 bits" + }, + "created": 1560919564, + "currency": "aud", + "image": null, + "inventory": { + "quantity": null, + "type": "infinite", + "value": null + }, + "livemode": false, + "metadata": { + }, + "package_dimensions": null, + "price": 500, + "product": "prod_FHUfY9DFwl0pPl", + "updated": 1560919796 + }, + "type": "sku" + } + ], + "livemode": false, + "locale": null, + "payment_intent": "pi_1Emw4WGf8y65MteUBrVOy4ME", + "payment_method_types": [ + "card" + ], + "submit_type": null, + "subscription": null, + "success_url": "http://localhost:40080/payments/success" + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "checkout.session.completed" +} \ No newline at end of file diff --git a/server/test/checkout.session.completed.subscription.json b/server/test/checkout.session.completed.subscription.json new file mode 100644 index 00000000..29a7e29e --- /dev/null +++ b/server/test/checkout.session.completed.subscription.json @@ -0,0 +1,63 @@ +{ + "id": "evt_1EmH4cGf8y65MteUj9qhzJzD", + "object": "event", + "api_version": "2019-05-16", + "created": 1560763462, + "data": { + "object": { + "id": "cs_test_RH4RVfajVXRAcruFssXLthaDnfVBJGiPumUfMzPdj5DixpieRl645hkQ", + "object": "checkout.session", + "billing_address_collection": null, + "cancel_url": "http://localhost:40080/payments/cancel", + "client_reference_id": "ff3bbecb-e744-4674-b411-a11d6832a5ac", + "customer": "cus_FGoioD9GGtTXlW", + "customer_email": null, + "display_items": [ + { + "amount": 1000, + "currency": "aud", + "plan": { + "id": "plan_FGmRwawcOJJ7Nv", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 1000, + "billing_scheme": "per_unit", + "created": 1560755040, + "currency": "aud", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": { + }, + "nickname": "basic", + "product": "prod_FGmRFYmB700pM5", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "type": "plan" + } + ], + "livemode": false, + "locale": null, + "payment_intent": null, + "payment_method_types": [ + "card" + ], + "submit_type": null, + "subscription": "sub_FGoiRaWHZUF01V", + "success_url": "http://localhost:40080/payments/success" + } + }, + "livemode": false, + "pending_webhooks": 1, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "checkout.session.completed" +} \ No newline at end of file diff --git a/server/test/checkout.session.completed.test.json b/server/test/checkout.session.completed.test.json new file mode 100644 index 00000000..16817050 --- /dev/null +++ b/server/test/checkout.session.completed.test.json @@ -0,0 +1,43 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "checkout.session.completed", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2019-05-16", + "data": { + "object": { + "id": "cs_00000000000000", + "object": "checkout.session", + "billing_address_collection": null, + "cancel_url": "https://example.com/cancel", + "client_reference_id": null, + "customer": null, + "customer_email": null, + "display_items": [ + { + "amount": 1500, + "currency": "usd", + "custom": { + "description": "Comfortable cotton t-shirt", + "images": null, + "name": "T-shirt" + }, + "quantity": 2, + "type": "custom" + } + ], + "livemode": false, + "locale": null, + "payment_intent": "pi_00000000000000", + "payment_method_types": [ + "card" + ], + "submit_type": null, + "subscription": null, + "success_url": "https://example.com/success" + } + } +} \ No newline at end of file diff --git a/server/test/customer.subscription.created.json b/server/test/customer.subscription.created.json new file mode 100644 index 00000000..effb3c2a --- /dev/null +++ b/server/test/customer.subscription.created.json @@ -0,0 +1,109 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.subscription.created", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2019-05-16", + "data": { + "object": { + "id": "sub_00000000000000", + "object": "subscription", + "application_fee_percent": null, + "billing": "charge_automatically", + "billing_cycle_anchor": 1560755136, + "billing_thresholds": null, + "cancel_at": null, + "cancel_at_period_end": false, + "canceled_at": null, + "collection_method": "charge_automatically", + "created": 1560755136, + "current_period_end": 1563347136, + "current_period_start": 1560755136, + "customer": "cus_00000000000000", + "days_until_due": null, + "default_payment_method": "pm_1EmEuIGf8y65MteU7aM67eNH", + "default_source": null, + "default_tax_rates": [ + ], + "discount": null, + "ended_at": null, + "items": { + "object": "list", + "data": [ + { + "id": "si_00000000000000", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1560755136, + "metadata": { + }, + "plan": { + "id": "plan_00000000000000", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 1000, + "billing_scheme": "per_unit", + "created": 1560755040, + "currency": "aud", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": { + }, + "nickname": "basic", + "product": "prod_00000000000000", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "subscription": "sub_00000000000000" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/subscription_items?subscription=sub_FGmToAWeV09Z35" + }, + "latest_invoice": "in_1EmEuKGf8y65MteUxapoByyd", + "livemode": false, + "metadata": { + }, + "plan": { + "id": "plan_00000000000000", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 1000, + "billing_scheme": "per_unit", + "created": 1560755040, + "currency": "aud", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": { + }, + "nickname": "basic", + "product": "prod_00000000000000", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "schedule": null, + "start": 1560755136, + "start_date": 1560755136, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + } + } +} \ No newline at end of file diff --git a/server/test/customer.subscription.updated.json b/server/test/customer.subscription.updated.json new file mode 100644 index 00000000..0e3974c2 --- /dev/null +++ b/server/test/customer.subscription.updated.json @@ -0,0 +1,134 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.subscription.updated", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2019-05-16", + "data": { + "object": { + "id": "sub_00000000000000", + "object": "subscription", + "application_fee_percent": null, + "billing": "charge_automatically", + "billing_cycle_anchor": 1560755136, + "billing_thresholds": null, + "cancel_at": null, + "cancel_at_period_end": false, + "canceled_at": null, + "collection_method": "charge_automatically", + "created": 1560755136, + "current_period_end": 1563347136, + "current_period_start": 1560755136, + "customer": "cus_00000000000000", + "days_until_due": null, + "default_payment_method": "pm_1EmEuIGf8y65MteU7aM67eNH", + "default_source": null, + "default_tax_rates": [ + ], + "discount": null, + "ended_at": null, + "items": { + "object": "list", + "data": [ + { + "id": "si_00000000000000", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1560755136, + "metadata": { + }, + "plan": { + "id": "plan_00000000000000", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 1000, + "billing_scheme": "per_unit", + "created": 1560755040, + "currency": "aud", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": { + }, + "nickname": "basic", + "product": "prod_00000000000000", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "subscription": "sub_00000000000000" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/subscription_items?subscription=sub_FGmToAWeV09Z35" + }, + "latest_invoice": "in_1EmEuKGf8y65MteUxapoByyd", + "livemode": false, + "metadata": { + }, + "plan": { + "id": "plan_00000000000000", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 1000, + "billing_scheme": "per_unit", + "created": 1560755040, + "currency": "aud", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": { + }, + "nickname": "basic", + "product": "prod_00000000000000", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "schedule": null, + "start": 1560755136, + "start_date": 1560755136, + "status": "active", + "tax_percent": null, + "trial_end": null, + "trial_start": null + }, + "previous_attributes": { + "plan": { + "id": "OLD_00000000000000", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 1000, + "billing_scheme": "per_unit", + "created": 1560755040, + "currency": "aud", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": { + }, + "nickname": "basic", + "product": "prod_00000000000000", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed", + "name": "Old plan" + } + } + } +} \ No newline at end of file diff --git a/server/test/subscription.json b/server/test/subscription.json deleted file mode 100644 index 495be9bc..00000000 --- a/server/test/subscription.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "object": { - "id": "sub_FGmgBxTVX2xM7H", - "object": "subscription", - "application_fee_percent": null, - "billing": "charge_automatically", - "billing_cycle_anchor": 1560755931, - "billing_thresholds": null, - "cancel_at": null, - "cancel_at_period_end": false, - "canceled_at": null, - "collection_method": "charge_automatically", - "created": 1560755931, - "current_period_end": 1563347931, - "current_period_start": 1560755931, - "customer": "cus_FGmgIHDimD5Tei", - "days_until_due": null, - "default_payment_method": "pm_1EmF78Gf8y65MteUQBHQU1po", - "default_source": null, - "default_tax_rates": [ - ], - "discount": null, - "ended_at": null, - "items": { - "object": "list", - "data": [ - { - "id": "si_FGmg6F7DZAyU2Y", - "object": "subscription_item", - "billing_thresholds": null, - "created": 1560755932, - "metadata": { - }, - "plan": { - "id": "plan_FGmRwawcOJJ7Nv", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 1000, - "billing_scheme": "per_unit", - "created": 1560755040, - "currency": "aud", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "nickname": "basic", - "product": "prod_FGmRFYmB700pM5", - "tiers": null, - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "quantity": 1, - "subscription": "sub_FGmgBxTVX2xM7H", - "tax_rates": [ - ] - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/subscription_items?subscription=sub_FGmgBxTVX2xM7H" - }, - "latest_invoice": "in_1EmF7AGf8y65MteUvH8MCela", - "livemode": false, - "metadata": { - }, - "plan": { - "id": "plan_FGmRwawcOJJ7Nv", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 1000, - "billing_scheme": "per_unit", - "created": 1560755040, - "currency": "aud", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "nickname": "basic", - "product": "prod_FGmRFYmB700pM5", - "tiers": null, - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "quantity": 1, - "schedule": null, - "start": 1560755931, - "start_date": 1560755931, - "status": "active", - "tax_percent": null, - "trial_end": null, - "trial_start": null - } -} \ No newline at end of file