├── .gitignore
├── LICENSE
├── README.md
├── client
├── billing.coffee
├── i18n
│ ├── arabic.coffee
│ ├── english.coffee
│ └── french.coffee
├── index.html
├── lib
│ └── parsley.css
├── startup.coffee
├── styles.less
└── views
│ ├── charges
│ ├── charges.coffee
│ ├── charges.html
│ └── charges.less
│ ├── creditCard
│ ├── creditCard.coffee
│ ├── creditCard.html
│ └── creditCard.less
│ ├── currentCreditCard
│ ├── currentCreditCard.coffee
│ └── currentCreditCard.html
│ └── invoices
│ ├── invoices.coffee
│ ├── invoices.html
│ └── invoices.less
├── collections
└── users.coffee
├── package.js
├── packages
└── .gitignore
├── public
└── img
│ ├── credit-cards.png
│ └── cvc.png
├── server
├── billing.coffee
├── methods.coffee
├── startup.coffee
└── webhooks.coffee
└── versions.json
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sublime-project
2 | *.sublime-workspace
3 | .build*
4 | .npm
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Differential
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | meteor-billing
2 | ==============
3 |
4 | Package for common billing functionality.
5 | See full [documentation](http://github.differential.com/meteor-billing/).
6 |
--------------------------------------------------------------------------------
/client/billing.coffee:
--------------------------------------------------------------------------------
1 | Result = ->
2 | _dep: new Deps.Dependency
3 | _val: null
4 | _hasRun: false
5 |
6 | _get: ->
7 | @_dep.depend()
8 | @_val
9 |
10 | _set: (val) ->
11 | unless EJSON.equals @_val, val
12 | @_val = val
13 | @_dep.changed()
14 |
15 |
16 | @Billing =
17 | settings:
18 | publishableKey: ''
19 | requireAddress: false
20 | requireName: false
21 | showInvoicePeriod: true
22 | showPricingPlan: true
23 | invoiceExplaination: ''
24 | currency: '$'
25 | language: 'en'
26 | ddBeforeMm: false #for countries with date format dd/mm/yyyy
27 |
28 | config: (opts) ->
29 | @settings = _.extend @settings, opts
30 | T9n.language = @settings.language
31 |
32 | isValid: ->
33 | $('form#billing-creditcard').parsley().validate()
34 |
35 | createToken: (form, callback) ->
36 | Stripe.setPublishableKey(@settings.publishableKey);
37 | $form = $(form)
38 | Stripe.card.createToken(
39 | name: $(form).find('[name=cc-name]').val()
40 | number: $form.find('[name=cc-num]').val()
41 | exp_month: $form.find('[name=cc-exp-month]').val()
42 | exp_year: $form.find('[name=cc-exp-year]').val()
43 | cvc: $form.find('[name=cc-cvc]').val()
44 | address_line1: $form.find('[name=cc-address-line-1]').val()
45 | address_line2: $form.find('[name=cc-address-line-2]').val()
46 | address_city: $form.find('[name=cc-address-city]').val()
47 | address_state: $form.find('[name=cc-address-state]').val()
48 | address_zip: $form.find('[name=cc-address-zip]').val()
49 | , callback) # callback(status, response)
50 |
51 | _results: {}
52 |
53 | getResults: (methodName) ->
54 | unless @_results[methodName]
55 | @_results[methodName] = new Result
56 |
57 | # Get the result
58 | res = @_results[methodName]._get()
59 |
60 | # If its an error object - return falsy value
61 | if res.error then res = null else res
62 |
63 | # Null out to ensure fresh data subsequent calls
64 | if res then @_results[methodName] = null
65 |
66 | # Return it!
67 | res
68 |
69 | call: (methodName, params) ->
70 | results = @_results
71 |
72 | unless results[methodName]
73 | results[methodName] = new Result
74 |
75 | unless results[methodName]._hasRun
76 | results[methodName]._hasRun = true
77 | args = Array.prototype.splice.call arguments, 1
78 | Meteor.apply methodName, args, (err, res) ->
79 | results[methodName]._set if err then err else res
80 |
81 | ready: ->
82 | res = results[methodName]._get()
83 | if res then true else false
--------------------------------------------------------------------------------
/client/i18n/arabic.coffee:
--------------------------------------------------------------------------------
1 | ar =
2 | # Billing
3 | "Past Invoices": "فواتير سابقة"
4 | "Upcoming Invoice": "فواتير قادمة"
5 | "Cancel Subscription": "الغاء الاشتراك"
6 | "Confirm Cancelling Subscription": "تأكيد الغاء الاشتراك"
7 | "Are you sure you want to cancel your subscription?": "هل انت متأكد انك تريد الغاء اشتراكك؟ سوف نحفظ بياناتك حتى تتمكن من اعادة حسابك فى اى وقت"
8 | "Cancel": "الغاء"
9 | "on": "على"
10 | "Yes, cancel my subscription": "نعم, الغى اشتراكى"
11 | "Line Items": "خط الاصناف"
12 | "Paid": "مدفوع"
13 | "month": "شهر"
14 | "Nothing to see here": "لا شيئ هنا"
15 |
16 | # Credit Card
17 | "Name": "الإسم"
18 | "Billing Info": "بيانات الدفع"
19 | "Card Number": "رقم كارت الائتمان"
20 | "Expiration": "تاريخ الانتهاء"
21 | "Card Code": "رمز الكارت السرى"
22 | "Subscription to": "الاشتراك ب"
23 | "Address": "العنوان"
24 |
25 | # Notifications
26 | "Error getting past invoices": "خطأ فى ايجاد الفواتير السابقة"
27 | "Error getting upcoming invoice": "خطأ فى ايجاد الفواتير القادمة"
28 | "Error canceling subscription": "خطأ فى الغاء الاشتراك"
29 | "Your subscription has been canceled": "تم الغاء اشتراكك"
30 |
31 | i18n.map "ar", ar
32 |
--------------------------------------------------------------------------------
/client/i18n/english.coffee:
--------------------------------------------------------------------------------
1 | en =
2 | # Billing
3 | "Past Charges": "Past Charges"
4 | "Past Invoices": "Past Invoices"
5 | "Upcoming Invoice": "Upcoming Invoice"
6 | "Cancel Subscription": "Cancel Subscription"
7 | "Confirm Cancelling Subscription": "Confirm Cancelling Subscription"
8 | "Are you sure you want to cancel your subscription?": "Are you sure you want to cancel your subscription? Your data will be saved, so you can re-enable your account at any time."
9 | "Cancel": "Cancel"
10 | "on": "on"
11 | "Yes, cancel my subscription": "Yes, cancel my subscription"
12 | "Line Items": "Line Items"
13 | "Paid": "Paid"
14 | "month": "month"
15 | "Nothing to see here": "Nothing to see here."
16 |
17 | # Credit Card
18 | "Name": "Name"
19 | "Billing Info": "Billing Info"
20 | "Card Number": "Card Number"
21 | "Expiration": "Expiration"
22 | "Card Code": "Card Code"
23 | "Subscription to": "Subscription to"
24 | "Address": "Address"
25 |
26 | # Notifications
27 | "Error getting past charges": "Error getting past charges."
28 | "Error getting past invoices": "Error getting past invoices."
29 | "Error getting upcoming invoice": "Error getting upcoming invoice."
30 | "Error canceling subscription": "Error canceling subscription."
31 | "Your subscription has been canceled": "Your subscription has been canceled."
32 |
33 | i18n.map "en", en
34 |
--------------------------------------------------------------------------------
/client/i18n/french.coffee:
--------------------------------------------------------------------------------
1 | fr=
2 | "Past Charges": "Transactions passées",
3 | "Past Invoices": "Reçus",
4 | "Upcoming Invoice": "Prochain Reçu",
5 | "Cancel Subscription": "Annuler la souscription",
6 | "Confirm Cancelling Subscription": "Confirmez l'annulation de la souscription",
7 | "Are you sure you want to cancel your subscription?": "Etes-vous sur de vouloir annuler la souscription? Vos données sont sauvegardées, vous pouvez souscrire de nouveau à tout moment",
8 | "Cancel": "Annuler",
9 | "on": "sur",
10 | "Yes, cancel my subscription": "Oui, annulez ma souscription",
11 | "Line Items": "Line Items",
12 | "Paid": "Payé",
13 | "month": "mois",
14 | "Nothing to see here": "Rien à signaler.",
15 | "Name": "Nom",
16 | "Billing Info": "Informations",
17 | "Card Number": "Numéro",
18 | "Expiration": "Date d'expiration",
19 | "Card Code": "Code CCV",
20 | "Subscription to": "Souscription à",
21 | "Address": "Adresse",
22 | "Error getting past charges": "Erreur d'accès aux transactions passées.",
23 | "Error getting past invoices": "Erreur d'accès aux reçus passées.",
24 | "Error getting upcoming invoice": "Erreur d'accès au prochain reçu.",
25 | "Error canceling subscription": "Erreur survenue pour annuler votre souscription.",
26 | "Your subscription has been canceled": "Votre souscription a bien été annulée."
27 |
28 | i18n.map "fr", fr
29 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/client/lib/parsley.css:
--------------------------------------------------------------------------------
1 | input.parsley-success,
2 | select.parsley-success,
3 | textarea.parsley-success {
4 | color: #468847;
5 | background-color: #DFF0D8;
6 | border: 1px solid #D6E9C6;
7 | }
8 |
9 | input.parsley-error,
10 | select.parsley-error,
11 | textarea.parsley-error {
12 | color: #B94A48;
13 | background-color: #F2DEDE;
14 | border: 1px solid #EED3D7;
15 | }
16 |
17 | .parsley-errors-list {
18 | margin: 2px 0 3px 0;
19 | padding: 0;
20 | list-style-type: none;
21 | font-size: 0.9em;
22 | line-height: 0.9em;
23 | opacity: 0;
24 | -moz-opacity: 0;
25 | -webkit-opacity: 0;
26 |
27 | transition: all .3s ease-in;
28 | -o-transition: all .3s ease-in;
29 | -ms-transition: all .3s ease-in-;
30 | -moz-transition: all .3s ease-in;
31 | -webkit-transition: all .3s ease-in;
32 | }
33 |
34 | .parsley-errors-list.filled {
35 | opacity: 1;
36 | }
37 |
--------------------------------------------------------------------------------
/client/startup.coffee:
--------------------------------------------------------------------------------
1 | Meteor.startup ->
2 | Meteor.subscribe 'currentUser'
--------------------------------------------------------------------------------
/client/styles.less:
--------------------------------------------------------------------------------
1 | // .error-msg {
2 | // color: #b94a48;
3 | // font-size: .9em;
4 | // }
5 |
6 | ul.parsley-error-list{
7 | font-size: .8em;
8 | color: #787878;
9 | font-style: italic;
10 | list-style-type: none;
11 | padding: 0;
12 | margin: 0;
13 | }
--------------------------------------------------------------------------------
/client/views/charges/charges.coffee:
--------------------------------------------------------------------------------
1 | Template.charges.created = ->
2 | Session.set 'charges.error', null
3 | Session.set 'charges.success', null
4 | Session.set 'charges.charges', null
5 |
6 | Template.charges.rendered = ->
7 | usr = BillingUser.current()
8 |
9 | unless usr then return
10 |
11 | if not Session.get 'charges.charges'
12 | Session.set 'charges.working', true
13 | Meteor.call 'listCharges', (error, response) ->
14 | Session.set 'charges.working', false
15 | if error
16 | if error.error is 404
17 | Session.set 'charges.charges', null
18 | else
19 | Session.set 'charges.error', t9n('Error getting charges')
20 | else
21 | Session.set 'charges.charges', response.data
22 |
23 | inDollars = (amt) ->
24 | currency = Billing.settings.currency
25 | if amt >= 0
26 | "#{currency}#{(amt / 100).toFixed(2)}"
27 | else
28 | "-#{currency}#{Math.abs(amt / 100).toFixed(2)}"
29 |
30 | formatDate = (timestamp) ->
31 | d = new Date(timestamp * 1000)
32 | monthOrDay = [d.getMonth()+1, d.getDate()]
33 | if Billing.settings.ddBeforeMm
34 | monthOrDay.reverse()
35 | monthOrDay.join('/') + '/' + d.getFullYear()
36 |
37 |
38 | Template.charges.helpers
39 | billingError: ->
40 | Session.get 'charges.error'
41 |
42 | billingSuccess: ->
43 | Session.get 'charges.success'
44 |
45 | charges: ->
46 | Session.get 'charges.charges'
47 |
48 | chargesWorking: ->
49 | Session.get 'charges.working'
50 |
51 |
52 | Template._charge.helpers
53 | chargeDate: (timestamp) ->
54 | formatDate(timestamp)
55 |
56 | chargeAmt: (amt) ->
57 | inDollars(amt)
58 |
--------------------------------------------------------------------------------
/client/views/charges/charges.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#if billingError}}
4 |
5 | {{billingError}}
6 |
7 | {{/if}}
8 | {{#if billingSuccess}}
9 |
10 | {{billingSuccess}}
11 |
12 | {{/if}}
13 |
14 |
{{i18n "Past Charges"}}
15 |
16 | {{#each charges}}
17 | {{> _charge}}
18 | {{else}}
19 | {{#if chargesWorking}}
20 |
21 | {{else}}
22 | {{i18n "Nothing to see here"}}
23 | {{/if}}
24 | {{/each}}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
48 |
49 |
50 |
51 | {{description}}
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/client/views/charges/charges.less:
--------------------------------------------------------------------------------
1 | .charges{
2 |
3 | .panel {
4 | .panel-heading{
5 | padding: 10px 8px;
6 | a{
7 | text-decoration: none;
8 | }
9 | }
10 | .panel-body{
11 | padding: 8px;
12 | }
13 | }
14 |
15 | .billing-loader{
16 | color: rgba(5, 5, 5, 0.2);
17 | text-align: center;
18 | }
19 |
20 | .charge{
21 | font-size: 1em;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/client/views/creditCard/creditCard.coffee:
--------------------------------------------------------------------------------
1 | Template.creditCard.helpers
2 | requireAddress: ->
3 | Billing.settings.requireAddress
4 |
5 | requireName: ->
6 | Billing.settings.requireName
--------------------------------------------------------------------------------
/client/views/creditCard/creditCard.html:
--------------------------------------------------------------------------------
1 |
2 |
78 |
79 |
--------------------------------------------------------------------------------
/client/views/creditCard/creditCard.less:
--------------------------------------------------------------------------------
1 | .credit-card{
2 | img {
3 | height: 20px;
4 | margin-left: 10px;
5 | }
6 |
7 | label {
8 | img {
9 | height: 15px;
10 | margin-left: 10px;
11 | position: relative;
12 | top: -2px;
13 | }
14 | }
15 |
16 | .expiration {
17 |
18 | input {
19 | text-align: center;
20 | }
21 |
22 | span {
23 | position: absolute;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/client/views/currentCreditCard/currentCreditCard.coffee:
--------------------------------------------------------------------------------
1 | currentCardComputation = null
2 |
3 | Template.currentCreditCard.rendered = ->
4 | currentCardComputation = Deps.autorun ->
5 | billing = Meteor.user().billing
6 | if billing and billing.customerId and billing.cardId
7 | Meteor.call 'retrieveCard', Meteor.userId(), (err, response) ->
8 | unless err then Session.set 'currentCreditCard.card', response
9 |
10 | Template.currentCreditCard.helpers
11 | card: ->
12 | Session.get 'currentCreditCard.card'
13 |
14 | Template.currentCreditCard.destroyed = ->
15 | if currentCardComputation then currentCardComputation.stop()
16 |
--------------------------------------------------------------------------------
/client/views/currentCreditCard/currentCreditCard.html:
--------------------------------------------------------------------------------
1 |
2 | {{#with card}}
3 |
4 |
5 | {{type}} ending in {{last4}}
6 |
7 |
8 | Expires: {{exp_month}} / {{exp_year}}
9 |
10 |
11 | {{/with}}
12 |
--------------------------------------------------------------------------------
/client/views/invoices/invoices.coffee:
--------------------------------------------------------------------------------
1 | Template.invoices.created = ->
2 | Session.set 'invoices.error', null
3 | Session.set 'invoices.success', null
4 | Session.set 'invoices.invoices.past', null
5 | Session.set 'invoices.invoices.upcoming', null
6 |
7 | Template.invoices.rendered = ->
8 | usr = BillingUser.current()
9 |
10 | unless usr then return
11 |
12 | if usr.billing and not Session.get 'invoices.invoices.past'
13 | Session.set 'invoices.past.working', true
14 | Meteor.call 'getInvoices', (error, response) ->
15 | Session.set 'invoices.past.working', false
16 | if error
17 | if error.error is 404
18 | Session.set 'invoices.invoices.past', null
19 | else
20 | Session.set 'invoices.error', t9n('Error getting past invoices')
21 | else
22 | Session.set 'invoices.invoices.past', response.data
23 |
24 | if usr.billing and usr.billing.subscriptionId and not Session.get 'invoices.invoices.upcoming'
25 | Session.set 'invoices.upcoming.working', true
26 | Meteor.call 'getUpcomingInvoice', (error, response) ->
27 | Session.set 'invoices.upcoming.working', false
28 | if error
29 | if error.error is 404
30 | Session.set 'invoices.invoices.upcoming', null
31 | else
32 | Session.set 'invoices.error', t9n('Error getting upcoming invoice')
33 | else
34 | response.id = new Meteor.Collection.ObjectID().toHexString()
35 | Session.set 'invoices.invoices.upcoming', response
36 |
37 | inDollars = (amt) ->
38 | currency = Billing.settings.currency
39 | if amt >= 0
40 | "#{currency}#{(amt / 100).toFixed(2)}"
41 | else
42 | "-#{currency}#{Math.abs(amt / 100).toFixed(2)}"
43 |
44 | formatDate = (timestamp) ->
45 | d = new Date(timestamp * 1000)
46 | monthOrDay = [d.getMonth()+1, d.getDate()]
47 | if Billing.settings.ddBeforeMm
48 | monthOrDay.reverse()
49 | monthOrDay.join('/') + '/' + d.getFullYear()
50 |
51 |
52 | Template.invoices.helpers
53 | billingError: ->
54 | Session.get 'invoices.error'
55 |
56 | billingSuccess: ->
57 | Session.get 'invoices.success'
58 |
59 | cancelingSubscription: ->
60 | Session.get 'invoices.cancelingSubscription'
61 |
62 | invoices: ->
63 | Session.get 'invoices.invoices.past'
64 |
65 | upcomingInvoice: ->
66 | Session.get 'invoices.invoices.upcoming'
67 |
68 | showPricingPlan: ->
69 | Billing.settings.showPricingPlan
70 |
71 | pricingPlan: ->
72 | upcomingInvoice = Session.get 'invoies.invoices.upcoming'
73 | if upcomingInvoice
74 | sub = _.findWhere upcomingInvoice.lines.data, type: 'subscription'
75 | plan = sub.plan
76 | "#{inDollars(plan.amount)}/#{t9n(plan.interval)}"
77 |
78 | hasSubscription: ->
79 | usr = BillingUser.current()
80 | usr and usr.billing and BillingUser.current().billing.subscriptionId
81 |
82 | pastInvoicesWorking: ->
83 | Session.get 'invoices.past.working'
84 |
85 | upcomingInvoiceWorking: ->
86 | Session.get 'invoices.upcoming.working'
87 |
88 |
89 | Template.invoices.events
90 | 'click #cancel-subscription': (e) ->
91 | e.preventDefault()
92 | $('#confirm-cancel-subscription').modal('show')
93 |
94 |
95 | #
96 | # Cancel Subscription Modal
97 | #
98 | Template.cancelSubscriptionModal.created = ->
99 | Session.set 'invoices.cancelingSubscription', false
100 |
101 | Template.cancelSubscriptionModal.events
102 | 'click .btn-confirm-cancel-subscription': (e) ->
103 | e.preventDefault()
104 | $('#confirm-cancel-subscription').modal('hide')
105 | Session.set 'invoices.cancelingSubscription', true
106 | Meteor.call 'cancelSubscription', Meteor.user().billing.customerId, (error, response) ->
107 | Session.set 'invoices.cancelingSubscription', false
108 | if error
109 | Session.set 'invoices.error', t9n('Error canceling subscription')
110 | else
111 | Session.set 'invoices.success', t9n('Your subscription has been canceled')
112 |
113 |
114 | Template._invoice.helpers
115 | lineItemDescription: ->
116 | if @type is 'subscription'
117 | "#{t9n("Subscription to")} #{@plan.name} (#{inDollars(@plan.amount)}/#{t9n(@plan.interval)})"
118 | else if @type is 'invoiceitem'
119 | @description
120 |
121 | lineItemPeriod: ->
122 | if @period.start is @period.end then formatDate(@period.start)
123 | else "#{formatDate(@period.start)} - #{formatDate(@period.end)}"
124 |
125 | invoiceDate: (timestamp) ->
126 | formatDate(timestamp)
127 |
128 | invoiceAmt: (amt) ->
129 | inDollars(amt)
130 |
131 | showInvoicePeriod: ->
132 | Billing.settings.showInvoicePeriod
133 |
134 | invoiceExplaination: ->
135 | Billing.settings.invoiceExplaination
--------------------------------------------------------------------------------
/client/views/invoices/invoices.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#if billingError}}
4 |
5 |
6 | {{billingError}}
7 |
8 |
9 | {{/if}}
10 | {{#if billingSuccess}}
11 |
12 |
13 | {{billingSuccess}}
14 |
15 |
16 | {{/if}}
17 |
18 |
19 |
20 |
{{i18n "Past Invoices"}}
21 |
22 |
23 | {{#each invoices}}
24 | {{> _invoice}}
25 | {{else}}
26 | {{#if pastInvoicesWorking}}
27 |
28 | {{else}}
29 | {{i18n "Nothing to see here"}}
30 | {{/if}}
31 | {{/each}}
32 |
33 |
34 |
35 |
36 |
{{i18n "Upcoming Invoice"}}
37 |
38 |
39 | {{#with upcomingInvoice}}
40 | {{> _invoice}}
41 | {{else}}
42 | {{#if upcomingInvoicesWorking}}
43 |
44 | {{else}}
45 | {{i18n "Nothing to see here"}}
46 | {{/if}}
47 | {{/with}}
48 |
49 |
50 |
51 |
52 |
53 |
54 | {{#if hasSubscription}}
55 |
73 | {{> cancelSubscriptionModal}}
74 | {{/if}}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
87 |
88 | {{i18n "Are you sure you want to cancel your subscription?"}}
89 |
90 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
119 |
120 |
121 |
122 | {{i18n "Line Items"}}
123 | {{#if paid}}
124 | {{i18n "Paid"}}
125 | {{/if}}
126 |
127 | {{#each lines.data}}
128 |
129 |
130 | {{lineItemDescription}}
131 |
132 |
133 | {{lineItemPeriod}}
134 |
135 |
136 | {{invoiceAmt amount}}
137 |
138 |
139 | {{/each}}
140 | {{#unless paid}}
141 |
142 |
143 |
144 | {{#if invoiceExplaination}}
145 | * {{invoiceExplaination}}
146 | {{/if}}
147 |
148 |
149 |
150 | {{/unless}}
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/client/views/invoices/invoices.less:
--------------------------------------------------------------------------------
1 | .invoices{
2 |
3 | .panel {
4 | .panel-heading{
5 | padding: 10px 8px;
6 | a{
7 | text-decoration: none;
8 | }
9 | }
10 | .panel-body{
11 | padding: 8px;
12 |
13 | .billing-disclaimer{
14 | padding-top: 30px;
15 | }
16 | }
17 | }
18 |
19 | .billing-loader{
20 | color: rgba(5, 5, 5, 0.2);
21 | text-align: center;
22 | }
23 |
24 | .billing-footer{
25 | margin-top: 80px;
26 | .price{
27 | text-align: right;
28 | margin: 5px 0px;
29 | font-weight: 500;
30 | font-size: 30px;
31 | }
32 | }
33 |
34 | .invoice{
35 | font-size: .7em;
36 | }
37 | }
--------------------------------------------------------------------------------
/collections/users.coffee:
--------------------------------------------------------------------------------
1 | class BillingUser extends Minimongoid
2 | @_collection: Meteor.users
3 |
4 | @current: ->
5 | @first _id: Meteor.userId()
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | Package.describe({
2 | name: "ryw:billing",
3 | summary: "Various billing functionality packaged up.",
4 | version: "2.0.2",
5 | git: "https://github.com/Differential/meteor-billing"
6 | });
7 |
8 | Npm.depends({
9 | stripe: "2.8.0"
10 | });
11 |
12 | Package.on_use(function (api, where) {
13 | api.versionsFrom("METEOR@0.9.0");
14 |
15 | api.use([
16 | 'templating',
17 | 'less',
18 | 'jquery',
19 | 'deps',
20 | 'natestrauser:parsleyjs@1.1.7',
21 | 'anti:i18n@0.4.3'
22 | ], 'client');
23 |
24 | api.use([
25 | 'accounts-password',
26 | 'arunoda:npm@0.2.6',
27 | 'hellogerard:reststop2@0.5.9'
28 | ], 'server');
29 |
30 | api.use([
31 | 'coffeescript',
32 | 'mrt:minimongoid@0.8.3'
33 | ], ['client', 'server']);
34 |
35 |
36 | api.addFiles([
37 | 'collections/users.coffee'
38 | ], ['client', 'server']);
39 |
40 | api.addFiles([
41 | 'client/views/creditCard/creditCard.html',
42 | 'client/views/creditCard/creditCard.less',
43 | 'client/views/creditCard/creditCard.coffee',
44 | 'client/views/invoices/invoices.html',
45 | 'client/views/invoices/invoices.coffee',
46 | 'client/views/invoices/invoices.less',
47 | 'client/views/charges/charges.html',
48 | 'client/views/charges/charges.coffee',
49 | 'client/views/charges/charges.less',
50 | 'client/views/currentCreditCard/currentCreditCard.html',
51 | 'client/views/currentCreditCard/currentCreditCard.coffee',
52 | 'client/lib/parsley.css',
53 | 'client/startup.coffee',
54 | 'client/billing.coffee',
55 | 'client/index.html',
56 | 'client/styles.less',
57 | 'public/img/credit-cards.png',
58 | 'public/img/cvc.png',
59 | 'client/i18n/english.coffee',
60 | 'client/i18n/arabic.coffee',
61 | 'client/i18n/french.coffee'
62 | ], 'client');
63 |
64 | api.addFiles([
65 | 'server/startup.coffee',
66 | 'server/billing.coffee',
67 | 'server/methods.coffee',
68 | 'server/webhooks.coffee'
69 | ], 'server');
70 |
71 | api.export('BillingUser', ['server', 'client']);
72 | api.export('i18n', 'client');
73 |
74 | });
75 |
--------------------------------------------------------------------------------
/packages/.gitignore:
--------------------------------------------------------------------------------
1 | /meteor-billing
2 | /stripe
3 |
--------------------------------------------------------------------------------
/public/img/credit-cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Differential/meteor-billing/589e53c0df11c5ee030468e78287c94588829e29/public/img/credit-cards.png
--------------------------------------------------------------------------------
/public/img/cvc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Differential/meteor-billing/589e53c0df11c5ee030468e78287c94588829e29/public/img/cvc.png
--------------------------------------------------------------------------------
/server/billing.coffee:
--------------------------------------------------------------------------------
1 | @Billing =
2 | settings: {}
3 | config: (opts) ->
4 | defaults =
5 | secretKey: ''
6 | @settings = _.extend defaults, opts
--------------------------------------------------------------------------------
/server/methods.coffee:
--------------------------------------------------------------------------------
1 | wrap = (resource, method, params) ->
2 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
3 | call = Async.wrap Stripe[resource], method
4 | try
5 | call params
6 | catch e
7 | console.error e
8 | throw new Meteor.Error 500, e.message
9 |
10 |
11 | Meteor.methods
12 |
13 | #
14 | # Creates stripe customer then updates the user document with the stripe customerId and cardId
15 | #
16 | createCustomer: (userId, card) ->
17 | console.log 'Creating customer for', userId
18 | user = BillingUser.first(_id: userId)
19 | unless user then throw new Meteor.Error 404, "User not found. Customer cannot be created."
20 |
21 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
22 | create = Async.wrap Stripe.customers, 'create'
23 | try
24 | email = if user.emails then user.emails[0].address else ''
25 | customer = create email: email, card: card.id
26 | Meteor.users.update _id: user._id,
27 | $set: 'billing.customerId': customer.id, 'billing.cardId': customer.default_card
28 | catch e
29 | console.error e
30 | throw new Meteor.Error 500, e.message
31 |
32 | #
33 | # Create a card on a customer and set cardId
34 | #
35 | createCard: (userId, card) ->
36 | console.log 'Creating card for', userId
37 | user = BillingUser.first _id: userId
38 | unless user then throw new Meteor.Error 404, "User not found. Card cannot be created."
39 |
40 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
41 | createCard = Async.wrap Stripe.customers, 'createCard'
42 | try
43 | card = createCard user.billing.customerId, card: card.id
44 | user.update('billing.cardId': card.id)
45 | catch e
46 | console.error e
47 | throw new Meteor.Error 500, e.message
48 |
49 | #
50 | # Get details about a customers credit card
51 | #
52 | retrieveCard: (userId) ->
53 | console.log "Retrieving card for #{userId}"
54 | user = BillingUser.first _id: userId
55 | unless user then throw new Meteor.Error 404, "User not found. Cannot retrieve card info."
56 |
57 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
58 | retrieveCard = Async.wrap Stripe.customers, 'retrieveCard'
59 | try
60 | retrieveCard user.billing.customerId, user.billing.cardId
61 | catch e
62 | console.log e
63 | throw new Meteor.Error 500, e.message
64 |
65 | #
66 | # Delete a card on customer and unset cardId
67 | #
68 | deleteCard: (userId) ->
69 | console.log 'Deleting card for', userId
70 | user = BillingUser.first _id: userId
71 | unless user then throw new Meteor.Error 404, "User not found. Card cannot be deleted."
72 |
73 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
74 | deleteCard = Async.wrap Stripe.customers, 'deleteCard'
75 | try
76 | card = deleteCard user.billing.customerId, user.billing.cardId
77 | user.update('billing.cardId': null)
78 | catch e
79 | console.error e
80 | throw new Meteor.Error 500, e.message
81 |
82 | #
83 | # Create a single one-time charge
84 | #
85 | createCharge: (params) ->
86 | console.log "Creating charge"
87 | wrap 'charges', 'create', params
88 |
89 | #
90 | # List charges for user with any filters applied
91 | #
92 | listCharges: (filters) ->
93 | console.log "Getting past charges for", Meteor.userId()
94 | if Meteor.user().billing
95 | params = customer: Meteor.user().billing.customerId
96 | if filters
97 | params = _.extend params, filters
98 | wrap 'charges', 'list', params
99 | else
100 | throw new Meteor.Error 404, "Customer not found. Cannot list charges."
101 |
102 |
103 | #
104 | # Update stripe subscription for user with provided plan and quantitiy
105 | #
106 | updateSubscription: (userId, params) ->
107 | console.log 'Updating subscription for', userId
108 | user = BillingUser.first(_id: userId)
109 | if user then customerId = user.billing.customerId
110 | unless user and customerId then new Meteor.Error 404, "User not found. Subscription cannot be updated."
111 | if user.billing.waiveFees or user.billing.admin then return
112 |
113 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
114 | updateSubscription = Async.wrap Stripe.customers, 'updateSubscription'
115 | try
116 | subscription = updateSubscription customerId, params
117 | Meteor.users.update _id: userId,
118 | $set: 'billing.subscriptionId': subscription.id, 'billing.planId' : params.plan
119 | catch e
120 | console.error e
121 | throw new Meteor.Error 500, e.message
122 |
123 | #
124 | # Manually cancels the stripe subscription for the provided customerId
125 | #
126 | cancelSubscription: (customerId) ->
127 | console.log 'Canceling subscription for', customerId
128 | user = BillingUser.first('billing.customerId': customerId)
129 | unless user then new Meteor.Error 404, "User not found. Subscription cannot be canceled."
130 |
131 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
132 | cancelSubscription = Async.wrap Stripe.customers, 'cancelSubscription'
133 | try
134 | cancelSubscription customerId
135 | catch e
136 | console.error e
137 | throw new Meteor.Error 500, e.message
138 |
139 |
140 | #
141 | # A subscription was deleted from Stripe, remove subscriptionId and card from user.
142 | #
143 | subscriptionDeleted: (customerId) ->
144 | console.log 'Subscription deleted for', customerId
145 | user = BillingUser.first('billing.customerId': customerId)
146 | unless user then new Meteor.Error 404, "User not found. Subscription cannot be deleted."
147 | user.update 'billing.subscriptionId': null, 'billing.planId': null
148 |
149 |
150 | #
151 | # Get past invoices
152 | #
153 | getInvoices: ->
154 | console.log 'Getting past invoices for', Meteor.userId()
155 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
156 | if Meteor.user().billing
157 | customerId = Meteor.user().billing.customerId
158 | try
159 | invoices = Async.wrap(Stripe.invoices, 'list')(customer: customerId)
160 | catch e
161 | console.error e
162 | throw new Meteor.Error 500, e.message
163 | invoices
164 | else
165 | throw new Meteor.Error 404, "No subscription"
166 |
167 |
168 | #
169 | # Get next invoice
170 | #
171 | getUpcomingInvoice: ->
172 | console.log 'Getting upcoming invoice for', Meteor.userId()
173 | Stripe = Npm.require('stripe')(Billing.settings.secretKey)
174 | if Meteor.user().billing
175 | customerId = Meteor.user().billing.customerId
176 | try
177 | invoice = Async.wrap(Stripe.invoices, 'retrieveUpcoming')(customerId)
178 | catch e
179 | console.error e
180 | throw new Meteor.Error 500, e.message
181 | invoice
182 | else
183 | throw new Meteor.Error 404, "No subscription"
184 |
--------------------------------------------------------------------------------
/server/startup.coffee:
--------------------------------------------------------------------------------
1 | Meteor.startup ->
2 |
3 | # Publish user with billing object
4 | Meteor.publish 'currentUser', ->
5 | Meteor.users.find _id: @userId,
6 | fields: billing: 1
7 |
8 | # Patch any existing/new users up with an empty billing object
9 | cursor = Meteor.users.find()
10 | cursor.observe
11 | added: (usr) ->
12 | unless usr.billing
13 | Meteor.users.update _id: usr._id,
14 | $set: billing: {}
--------------------------------------------------------------------------------
/server/webhooks.coffee:
--------------------------------------------------------------------------------
1 | RESTstop.add 'webhooks', ->
2 | console.log 'Recieving Webhook --- ', @params.type
3 |
4 | switch @params.type
5 | when 'charge.failed'
6 | RESTstop.call @, 'cancelSubscription', @params.data.object.customer
7 | when 'customer.subscription.deleted'
8 | RESTstop.call @, 'subscriptionDeleted', @params.data.object.customer
9 | when 'customer.deleted'
10 | BillingUser.destroyAll 'profile.customerId': @params.data.object.id
11 | else console.log(@params.type, 'was ignored')
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": [
3 | [
4 | "accounts-base",
5 | "1.1.1"
6 | ],
7 | [
8 | "accounts-password",
9 | "1.0.2"
10 | ],
11 | [
12 | "anti:i18n",
13 | "0.4.3"
14 | ],
15 | [
16 | "application-configuration",
17 | "1.0.2"
18 | ],
19 | [
20 | "arunoda:npm",
21 | "0.2.6"
22 | ],
23 | [
24 | "base64",
25 | "1.0.0"
26 | ],
27 | [
28 | "binary-heap",
29 | "1.0.0"
30 | ],
31 | [
32 | "blaze",
33 | "2.0.1"
34 | ],
35 | [
36 | "blaze-tools",
37 | "1.0.0"
38 | ],
39 | [
40 | "boilerplate-generator",
41 | "1.0.0"
42 | ],
43 | [
44 | "callback-hook",
45 | "1.0.0"
46 | ],
47 | [
48 | "check",
49 | "1.0.1"
50 | ],
51 | [
52 | "coffeescript",
53 | "1.0.3"
54 | ],
55 | [
56 | "ddp",
57 | "1.0.9"
58 | ],
59 | [
60 | "deps",
61 | "1.0.4"
62 | ],
63 | [
64 | "ejson",
65 | "1.0.3"
66 | ],
67 | [
68 | "email",
69 | "1.0.3"
70 | ],
71 | [
72 | "follower-livedata",
73 | "1.0.1"
74 | ],
75 | [
76 | "geojson-utils",
77 | "1.0.0"
78 | ],
79 | [
80 | "hellogerard:reststop2",
81 | "0.5.9"
82 | ],
83 | [
84 | "html-tools",
85 | "1.0.1"
86 | ],
87 | [
88 | "htmljs",
89 | "1.0.1"
90 | ],
91 | [
92 | "id-map",
93 | "1.0.0"
94 | ],
95 | [
96 | "jquery",
97 | "1.0.0"
98 | ],
99 | [
100 | "json",
101 | "1.0.0"
102 | ],
103 | [
104 | "less",
105 | "1.0.9"
106 | ],
107 | [
108 | "localstorage",
109 | "1.0.0"
110 | ],
111 | [
112 | "logging",
113 | "1.0.3"
114 | ],
115 | [
116 | "meteor",
117 | "1.1.1"
118 | ],
119 | [
120 | "minifiers",
121 | "1.1.0"
122 | ],
123 | [
124 | "minimongo",
125 | "1.0.3"
126 | ],
127 | [
128 | "mongo",
129 | "1.0.6"
130 | ],
131 | [
132 | "mrt:minimongoid",
133 | "0.8.8"
134 | ],
135 | [
136 | "mrt:underscore-string-latest",
137 | "2.3.3"
138 | ],
139 | [
140 | "natestrauser:parsleyjs",
141 | "1.1.7"
142 | ],
143 | [
144 | "npm-bcrypt",
145 | "0.7.7"
146 | ],
147 | [
148 | "observe-sequence",
149 | "1.0.2"
150 | ],
151 | [
152 | "ordered-dict",
153 | "1.0.0"
154 | ],
155 | [
156 | "random",
157 | "1.0.0"
158 | ],
159 | [
160 | "reactive-var",
161 | "1.0.2"
162 | ],
163 | [
164 | "retry",
165 | "1.0.0"
166 | ],
167 | [
168 | "routepolicy",
169 | "1.0.1"
170 | ],
171 | [
172 | "service-configuration",
173 | "1.0.1"
174 | ],
175 | [
176 | "sha",
177 | "1.0.0"
178 | ],
179 | [
180 | "spacebars",
181 | "1.0.2"
182 | ],
183 | [
184 | "spacebars-compiler",
185 | "1.0.2"
186 | ],
187 | [
188 | "srp",
189 | "1.0.0"
190 | ],
191 | [
192 | "templating",
193 | "1.0.7"
194 | ],
195 | [
196 | "tracker",
197 | "1.0.2"
198 | ],
199 | [
200 | "ui",
201 | "1.0.3"
202 | ],
203 | [
204 | "underscore",
205 | "1.0.0"
206 | ],
207 | [
208 | "webapp",
209 | "1.1.2"
210 | ],
211 | [
212 | "webapp-hashing",
213 | "1.0.0"
214 | ]
215 | ],
216 | "pluginDependencies": [],
217 | "toolVersion": "meteor-tool@1.0.33",
218 | "format": "1.0"
219 | }
--------------------------------------------------------------------------------