Skip to content
This repository
Browse code

WHISTLE-1217: WIP

  • Loading branch information...
commit 5e6bfa20b7dfeaf5933142cc461c085a5ef01b8b 1 parent 9b9020b
bitbashing authored August 03, 2012

Showing 20 changed files with 741 additions and 178 deletions. Show diff stats Hide diff stats

  1. BIN  lib/braintree-1.0.0/ebin/braintree_addon.beam
  2. BIN  lib/braintree-1.0.0/ebin/braintree_address.beam
  3. BIN  lib/braintree-1.0.0/ebin/braintree_card.beam
  4. BIN  lib/braintree-1.0.0/ebin/braintree_customer.beam
  5. BIN  lib/braintree-1.0.0/ebin/braintree_discount.beam
  6. BIN  lib/braintree-1.0.0/ebin/braintree_request.beam
  7. BIN  lib/braintree-1.0.0/ebin/braintree_subscription.beam
  8. BIN  lib/braintree-1.0.0/ebin/braintree_transaction.beam
  9. BIN  lib/braintree-1.0.0/ebin/braintree_util.beam
  10. 6  lib/braintree-1.0.0/include/braintree.hrl
  11. 26  lib/braintree-1.0.0/src/braintree_customer.erl
  12. 44  lib/braintree-1.0.0/src/braintree_subscription.erl
  13. 47  lib/whistle_services-1.0.0/priv/example_account_services.json
  14. 181  lib/whistle_services-1.0.0/priv/example_service_plan.json
  15. 154  lib/whistle_services-1.0.0/src/bookkeepers/wh_bookkeeper_braintree.erl
  16. 23  lib/whistle_services-1.0.0/src/wh_service_invoice.erl
  17. 243  lib/whistle_services-1.0.0/src/wh_service_item.erl
  18. 102  lib/whistle_services-1.0.0/src/wh_service_items.erl
  19. 81  lib/whistle_services-1.0.0/src/wh_service_plan.erl
  20. 12  lib/whistle_services-1.0.0/src/wh_service_plans.erl
BIN  lib/braintree-1.0.0/ebin/braintree_addon.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_address.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_card.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_customer.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_discount.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_request.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_subscription.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_transaction.beam
Binary file not shown
BIN  lib/braintree-1.0.0/ebin/braintree_util.beam
Binary file not shown
6  lib/braintree-1.0.0/include/braintree.hrl
@@ -59,13 +59,14 @@
59 59
 -define(BT_TRANS_CVV, <<"cvv">>).
60 60
 -define(BT_TRANS_DUP, <<"duplicate">>).
61 61
 
62  
-
63 62
 -define(BT_ACTIVE, <<"Active">>).
64 63
 -define(BT_CANCELED, <<"Canceled">>).
65 64
 -define(BT_EXPIRED, <<"Expired">>).
66 65
 -define(BT_PAST_DUE, <<"Past Due">>).
67 66
 -define(BT_PENDING, <<"Pending">>).
68 67
 
  68
+-define(BT_ACTIVE_STATUSES, [?BT_ACTIVE, ?BT_PENDING, ?BT_PAST_DUE]). 
  69
+
69 70
 -record(bt_address, {id = 'undefined' :: 'undefined' | ne_binary()
70 71
                      ,customer_id = 'undefined' :: 'undefined' | ne_binary()
71 72
                      ,first_name = 'undefined' :: 'undefined' | ne_binary()
@@ -153,10 +154,9 @@
153 154
                           ,trial_period = 'undefined' :: 'undefined' | ne_binary()
154 155
                           ,add_ons = [] :: [#bt_addon{},...] | []
155 156
                           ,discounts = [] :: [#bt_discount{},...] | []
156  
-%%                          ,discounts = 'undefined' :: 'undefined' | ne_binary()
157 157
                           ,descriptor = 'undefined' :: 'undefined' | ne_binary()
158 158
                           ,transactions = 'undefined' :: 'undefined' | ne_binary()
159  
-                          ,do_not_inherit = 'false' :: boolean()
  159
+                          ,do_not_inherit = 'true' :: boolean()
160 160
                           ,start_immediately = 'true' :: boolean()
161 161
                           ,prorate_charges = 'true' :: boolean()
162 162
                           ,revert_on_prorate_fail = 'true' :: boolean()
26  lib/braintree-1.0.0/src/braintree_customer.erl
@@ -10,7 +10,7 @@
10 10
 
11 11
 -export([url/0, url/1]).
12 12
 -export([new/1]).
13  
--export([new_subscription/3]).
  13
+-export([new_subscription/2]).
14 14
 -export([default_payment_token/1]).
15 15
 -export([get_id/1]).
16 16
 -export([get_cards/1]).
@@ -61,10 +61,10 @@ new(CustomerId) ->
61 61
 %% Creates a new subscription record
62 62
 %% @end
63 63
 %%--------------------------------------------------------------------
64  
--spec new_subscription/3 :: (ne_binary(), ne_binary(), #bt_customer{}) -> #bt_subscription{}.
65  
-new_subscription(SubscriptionId, PlanId, Customer) ->
  64
+-spec new_subscription/2 :: (ne_binary(), #bt_customer{}) -> #bt_subscription{}.
  65
+new_subscription(PlanId, Customer) ->
66 66
     PaymentToken = default_payment_token(Customer),
67  
-    braintree_subscription:new(SubscriptionId, PlanId, PaymentToken).
  67
+    braintree_subscription:new(PlanId, PaymentToken).
68 68
 
69 69
 %%--------------------------------------------------------------------
70 70
 %% @public
@@ -113,11 +113,19 @@ get_subscriptions(#bt_customer{subscriptions=Subscriptions}) ->
113 113
 %% @end
114 114
 %%--------------------------------------------------------------------
115 115
 -spec get_subscription/2 :: (ne_binary(), #bt_customer{}) -> #bt_subscription{}.
116  
-get_subscription(SubscriptionId, Customer) ->
117  
-    case lists:keyfind(SubscriptionId, #bt_subscription.id, get_subscriptions(Customer)) of
118  
-        false -> braintree_util:error_not_found(<<"Subscription">>);
119  
-        Subscription -> Subscription
120  
-    end.
  116
+get_subscription(PlanId, #bt_customer{subscriptions=Subscriptions}) ->
  117
+    get_subscription(PlanId, Subscriptions);
  118
+get_subscription(_, []) ->
  119
+    braintree_util:error_not_found(<<"Subscription">>);
  120
+get_subscription(PlanId, [#bt_subscription{plan_id=PlanId, status=Status}=Subscription
  121
+                          |Subscriptions
  122
+                         ]) ->
  123
+    case lists:member(Status, ?BT_ACTIVE_STATUSES) of
  124
+        true -> Subscription;
  125
+        false -> get_subscription(PlanId, Subscriptions)
  126
+    end;
  127
+get_subscription(PlanId, [_|Subscriptions]) ->
  128
+    get_subscription(PlanId, Subscriptions).
121 129
 
122 130
 %%--------------------------------------------------------------------
123 131
 %% @public
44  lib/braintree-1.0.0/src/braintree_subscription.erl
@@ -9,9 +9,12 @@
9 9
 -module(braintree_subscription).
10 10
 
11 11
 -export([url/0, url/1, url/2]).
12  
--export([new/3]).
  12
+-export([new/2, new/3]).
13 13
 -export([get_id/1]).
14 14
 -export([get_addon/2]).
  15
+-export([reset/1]).
  16
+-export([reset_discounts/1]).
  17
+-export([reset_addons/1]).
15 18
 -export([get_addon_quantity/2]).
16 19
 -export([update_addon_amount/3]).
17 20
 -export([get_discount/2]).
@@ -63,7 +66,12 @@ url(SubscriptionId, Options) ->
63 66
 %% Creates a new subscription record
64 67
 %% @end
65 68
 %%--------------------------------------------------------------------
  69
+-spec new/2 :: (ne_binary(), ne_binary()) -> #bt_subscription{}.
66 70
 -spec new/3 :: (ne_binary(), ne_binary(), ne_binary()) -> #bt_subscription{}.
  71
+
  72
+new(PlanId, PaymentToken) ->
  73
+    new(wh_util:rand_hex_binary(16), PlanId, PaymentToken).
  74
+
67 75
 new(SubscriptionId, PlanId, PaymentToken) ->
68 76
     #bt_subscription{id=SubscriptionId
69 77
                      ,payment_token=PaymentToken
@@ -195,6 +203,8 @@ find(SubscriptionId) ->
195 203
 -spec create/1 :: (#bt_subscription{}) -> #bt_subscription{}.
196 204
 -spec create/2 :: (ne_binary(), ne_binary()) -> #bt_subscription{}.
197 205
 
  206
+create(#bt_subscription{id=undefined}=Subscription) ->
  207
+    create(Subscription#bt_subscription{id=wh_util:rand_hex_binary(16)});
198 208
 create(#bt_subscription{}=Subscription) ->
199 209
     Url = url(),
200 210
     Request = record_to_xml(Subscription, true),
@@ -236,6 +246,38 @@ cancel(SubscriptionId) ->
236 246
 %%--------------------------------------------------------------------
237 247
 %% @public
238 248
 %% @doc
  249
+%%
  250
+%% @end
  251
+%%--------------------------------------------------------------------
  252
+-spec reset/1 :: (#bt_subscription{}) -> #bt_subscription{}.
  253
+reset(Subscription) ->
  254
+    lists:foldl(fun(F, S) -> F(S) end, Subscription, [fun reset_addons/1
  255
+                                                      ,fun reset_discounts/1
  256
+                                                     ]).
  257
+
  258
+%%--------------------------------------------------------------------
  259
+%% @public
  260
+%% @doc
  261
+%%
  262
+%% @end
  263
+%%--------------------------------------------------------------------
  264
+-spec reset_addons/1 :: (#bt_subscription{}) -> #bt_subscription{}.
  265
+reset_addons(#bt_subscription{add_ons=AddOns}=Subscription) ->
  266
+    Subscription#bt_subscription{add_ons=[AddOn#bt_addon{quantity=0} || AddOn <- AddOns]}.
  267
+
  268
+%%--------------------------------------------------------------------
  269
+%% @public
  270
+%% @doc
  271
+%%
  272
+%% @end
  273
+%%--------------------------------------------------------------------
  274
+-spec reset_discounts/1 :: (#bt_subscription{}) -> #bt_subscription{}.
  275
+reset_discounts(#bt_subscription{discounts=Discounts}=Subscription) ->
  276
+    Subscription#bt_subscription{discounts=[Discount#bt_discount{quantity=0} || Discount <- Discounts]}.
  277
+
  278
+%%--------------------------------------------------------------------
  279
+%% @public
  280
+%% @doc
239 281
 %% Really ugly function to update an addon for a given subscription
240 282
 %% or subscription id
241 283
 %% @end
47  lib/whistle_services-1.0.0/priv/example_account_services.json
... ...
@@ -0,0 +1,47 @@
  1
+{
  2
+   "_id": "c0705d7984ea0160110a451b25a4406b",
  3
+   "pvt_created": 63511078846,
  4
+   "pvt_modified": 63511078847,
  5
+   "pvt_type": "service",
  6
+   "pvt_vsn": "1",
  7
+   "pvt_account_id": "c0705d7984ea0160110a451b25a4406b",
  8
+   "pvt_account_db": "account%2Fc0%2F70%2F5d7474ea0160c10a351b2544006b",
  9
+   "pvt_dirty": false,
  10
+   "quantities": {
  11
+       "number_features": {
  12
+           "cnam": 1,
  13
+           "dash_e911": 3
  14
+       },
  15
+       "phone_numbers": {
  16
+           "did_us": 9,
  17
+           "tollfree_us": 1
  18
+       },
  19
+       "limits": {
  20
+           "twoway_trunks": 2,
  21
+           "inbound_trunks": 1
  22
+       },
  23
+       "devices": {
  24
+           "cellphone": 1,
  25
+           "sip_device": 6
  26
+       }
  27
+   },
  28
+   "plans": {
  29
+       "968dc36503bcb05f798d9530016f311f": {
  30
+           "vendor_id": "c0705d7984ea0160110a451b25a4406b",
  31
+           "overrides": {
  32
+               "phone_numbers": {
  33
+                   "did_us": {
  34
+                       "discounts": {
  35
+                           "cumulative": {
  36
+                               "rate": 5
  37
+                           }
  38
+                       }
  39
+                   }
  40
+               }
  41
+           }
  42
+       }
  43
+   },
  44
+   "pvt_tree": [
  45
+       "c0705d7984ea0160110a451b25a4406b"
  46
+   ]
  47
+}
181  lib/whistle_services-1.0.0/priv/example_service_plan.json
... ...
@@ -1,81 +1,126 @@
1 1
 {
2 2
    "_id": "968dc36503bcb05f798d9530016f311f",
3  
-   "phone_numbers": {
4  
-       "^\\+{0,1}1{0,1}(800\\d{7})$": {
5  
-           "plan": "SIP_Services",
6  
-           "add_on": "tollfree_us",
7  
-           "activation_charge": "5.00",
8  
-           "combined_discounts": {
9  
-               "0-5": "discount_code_A",
10  
-               "6-10": "discount_code_B",
11  
-               "11-20": "discount_code_C"
  3
+   "name": "Direct Web Signups, No Support",
  4
+   "pvt_type": "service_plan",
  5
+   "description": "",
  6
+   "plan": {
  7
+       "phone_numbers": {
  8
+           "did_us": {
  9
+               "name": "US DID",
  10
+               "rate": 1,
  11
+               "discounts": {
  12
+                   "cumulative": {
  13
+                       "maximum": 2,
  14
+                       "rate": 0.5
  15
+                   }
  16
+               },
  17
+               "cascade": false
12 18
            },
13  
-           "exclusive_discounts": {
14  
-               "11-20": "discount_code_X",
15  
-               "21-49": "discount_code_Y",
16  
-               "50-99": "discount_code_Z"
  19
+           "tollfree_us": {
  20
+               "name": "US Tollfree",
  21
+               "rate": 5,
  22
+               "cascade": true,
  23
+               "minimum": 10
17 24
            }
18 25
        },
19  
-       "^\\+{0,1}1{0,1}(\\d{10})$": {
20  
-           "plan": "SIP_Services",
21  
-           "add_on": "did_us",
22  
-           "activation_charge": "20.00"
23  
-       }
24  
-   },
25  
-   "number_services": {
26  
-       "cnam": {
27  
-           "activation_charge": "2038.00"
28  
-       },
29  
-       "port": {
30  
-           "activation_charge": "200.00"
31  
-       },
32  
-       "dash_e911": {
33  
-           "activation_charge": "10.00",
34  
-           "plan": "SIP_Services",
35  
-           "add_on": "e911"
36  
-       },
37  
-       "failover": {
38  
-       }
39  
-   },
40  
-   "limits": {
41  
-       "twoway_trunks": {
42  
-           "plan": "SIP_Services",
43  
-           "add_on": "twoway_trunk"
  26
+       "number_services": {
  27
+           "cnam": {
  28
+               "name": "CNAM Update",
  29
+               "activation_charge": 2
  30
+           },
  31
+           "port": {
  32
+               "name": "Port Request",
  33
+               "activation_charge": 5
  34
+           },
  35
+           "e911": {
  36
+               "name": "E911 Service",
  37
+               "rate": 5,
  38
+               "discounts": {
  39
+                   "single": {
  40
+                       "rate": 5
  41
+                   }
  42
+               }
  43
+           }
44 44
        },
45  
-       "inbound_trunks": {
46  
-           "plan": "SIP_Services",
47  
-           "add_on": "inbound_trunk"
48  
-       }
49  
-   },
50  
-   "devices": {
51  
-       "cellphone": {
52  
-           "plan": "SIP_Services",
53  
-           "add_on": "virtual_extension",
54  
-           "activation_charge": "120.00"
  45
+       "limits": {
  46
+           "twoway_trunks": {
  47
+               "name": "Two-Way Trunk",
  48
+               "rate": 29.99
  49
+           },
  50
+           "inbound_trunks": {
  51
+               "name": "Inbound Trunk",
  52
+               "rate": 19.99
  53
+           }
55 54
        },
56  
-       "sip_device": {
57  
-           "plan": "SIP_Services",
58  
-           "add_on": "sip_device",
59  
-           "activation_charge": "18.99"
  55
+       "devices": {
  56
+           "_all": {
  57
+               "name": "SIP Device",
  58
+               "as": "sip_devices",
  59
+               "cascade": true,
  60
+               "rates": {
  61
+                   "5": 0,
  62
+                   "20": 24.95,
  63
+                   "50": 49.95,
  64
+                   "100": 149.95
  65
+               }
  66
+           }
60 67
        },
61  
-       "softphone": {
62  
-           "plan": "SIP_Services",
63  
-           "add_on": "softphone",
64  
-           "activation_charge": "18.99"
  68
+       "users": {
  69
+           "_all": {
  70
+               "name": "System User",
  71
+               "as": "users",
  72
+               "exceptions": [
  73
+                   "admins"
  74
+               ],
  75
+               "rates": {
  76
+                   "5": 0,
  77
+                   "20": 24.95,
  78
+                   "50": 49.95,
  79
+                   "100": 149.95
  80
+               }
  81
+           }
65 82
        }
66 83
    },
67  
-   "base_mrc": [
68  
-       {
69  
-           "plan": "SIP_Services",
70  
-           "add_on": "seats",
71  
-           "type": "device",
72  
-           "default_price": "50.00",
73  
-           "price_increments": {
74  
-               "5": 4.99,
75  
-               "10": 9.99,
76  
-               "25": 19.99,
77  
-               "50": 34.99
  84
+   "bookkeepers": {
  85
+       "braintree": {
  86
+           "phone_numbers": {
  87
+               "did_us": {
  88
+                   "plan": "SIP_Services",
  89
+                   "addon": "did_us",
  90
+                   "discounts": {
  91
+                       "cumulative": "discount_did_us"
  92
+                   }
  93
+               },
  94
+               "tollfree_us": {
  95
+                   "plan": "SIP_Services",
  96
+                   "addon": "tollfree_us"
  97
+               }
  98
+           },
  99
+           "number_services": {
  100
+               "e911": {
  101
+                   "plan": "SIP_Services",
  102
+                   "addon": "tollfree_us",
  103
+                   "discounts": {
  104
+                       "single": "discount_e911"
  105
+                   }
  106
+               }
  107
+           },
  108
+           "limits": {
  109
+               "twoway_trunks": {
  110
+                   "plan": "SIP_Services",
  111
+                   "addon": "twoway_trunk"
  112
+               },
  113
+               "inbound_trunks": {
  114
+                   "plan": "SIP_Services",
  115
+                   "addon": "inbound_trunk"
  116
+               }
  117
+           },
  118
+           "devices": {
  119
+               "sip_devices": {
  120
+                   "plan": "SIP_Services",
  121
+                   "addon": "sip_device"
  122
+               }
78 123
            }
79 124
        }
80  
-   ]
  125
+   }
81 126
 }
154  lib/whistle_services-1.0.0/src/bookkeepers/wh_bookkeeper_braintree.erl
@@ -7,15 +7,153 @@
7 7
 %%%-------------------------------------------------------------------
8 8
 -module(wh_bookkeeper_braintree).
9 9
 
  10
+-include_lib("whistle_services/src/whistle_services.hrl").
10 11
 
11  
-item_quantity(Category, Item, Quantity, Bookkeeper) ->
12  
-    ok.
  12
+-export([sync/2]).
13 13
 
14  
-item_price(Category, Item, Quantity, Bookkeeper) ->
15  
-    ok.
  14
+-record(wh_service_update, {bt_subscription
  15
+                            ,plan_id
  16
+                           }).
16 17
 
17  
-discount_quantity(Category, Item, Quantity, Bookkeeper) ->
18  
-    ok.
  18
+-record(wh_service_updates, {bt_subscriptions = []
  19
+                             ,billing_id
  20
+                             ,bt_customer
  21
+                            }).
  22
+                             
  23
+%%--------------------------------------------------------------------
  24
+%% @public
  25
+%% @doc
  26
+%%
  27
+%% @end
  28
+%%--------------------------------------------------------------------
  29
+sync(Items, BillingId) ->
  30
+    Customer = fetch_or_create_customer(BillingId),
  31
+    sync(wh_service_items:to_list(Items), BillingId, #wh_service_updates{bt_customer=Customer}).
19 32
 
20  
-discount_price(Category, Item, Quantity, Bookkeeper) ->
21  
-    ok.
  33
+sync([], _BillingId, #wh_service_updates{bt_subscriptions=Subscriptions}) ->
  34
+    _ = [braintree_subscription:update(Subscription) 
  35
+         || #wh_service_update{bt_subscription=Subscription} <- Subscriptions
  36
+        ],
  37
+    ok;
  38
+sync([ServiceItem|ServiceItems], BillingId, Updates) ->
  39
+    case braintree_plan_addon_id(ServiceItem) of
  40
+        {undefined, _} -> sync(ServiceItems, BillingId, Updates);
  41
+        {_, undefined} -> sync(ServiceItems, BillingId, Updates);
  42
+        {PlanId, AddOnId}->
  43
+            Quantity = wh_service_item:quantity(ServiceItem),
  44
+            Routines = [fun(S) -> braintree_subscription:update_addon_quantity(S, AddOnId, Quantity) end
  45
+                        ,fun(S) -> handle_single_discount(ServiceItem, S) end
  46
+                        ,fun(S) -> handle_cumulative_discounts(ServiceItem, S) end
  47
+                       ],
  48
+            Subscription = lists:foldl(fun(F, S) -> F(S) end, fetch_or_create_subscription(PlanId, Updates), Routines),
  49
+            sync(ServiceItems, BillingId, update_subscriptions(PlanId, Subscription, Updates))
  50
+    end.
  51
+
  52
+handle_single_discount(ServiceItem, Subscription) ->
  53
+    DiscountId = braintree_single_discount_id(ServiceItem),
  54
+    SingleDiscount = wh_service_item:single_discount(ServiceItem),
  55
+    case wh_util:is_empty(SingleDiscount) orelse wh_util:is_empty(DiscountId) of
  56
+        true -> Subscription;
  57
+        false ->
  58
+            S = braintree_subscription:update_discount_quantity(Subscription, DiscountId, 1),
  59
+            case wh_service_item:single_discount_rate(ServiceItem) of
  60
+                undefined -> S;
  61
+                Rate -> braintree_subscription:update_discount_amount(S, DiscountId, Rate)
  62
+            end
  63
+    end.
  64
+    
  65
+handle_cumulative_discounts(ServiceItem, Subscription) ->
  66
+    DiscountId = braintree_cumulative_discount_id(ServiceItem),
  67
+    CumulativeDiscount = wh_service_item:cumulative_discount(ServiceItem),
  68
+    case wh_util:is_empty(CumulativeDiscount) orelse wh_util:is_empty(DiscountId) of
  69
+        true -> Subscription;
  70
+        false ->
  71
+            S = braintree_subscription:update_discount_quantity(Subscription, DiscountId, CumulativeDiscount),
  72
+            case wh_service_item:cumulative_discount_rate(ServiceItem) of
  73
+                undefined -> S;
  74
+                Rate -> braintree_subscription:update_discount_amount(S, DiscountId, Rate)
  75
+            end
  76
+    end.
  77
+
  78
+%%--------------------------------------------------------------------
  79
+%% @private
  80
+%% @doc
  81
+%%
  82
+%% @end
  83
+%%--------------------------------------------------------------------
  84
+-spec fetch_or_create_customer/1 :: (ne_binary()) -> braintree_customer:customer().
  85
+fetch_or_create_customer(BillingId) ->
  86
+    lager:debug("requesting braintree customer ~s", [BillingId]),
  87
+    try braintree_customer:find(BillingId) of
  88
+        Customer -> Customer
  89
+    catch
  90
+        throw:{not_found, _} ->
  91
+            lager:debug("creating new braintree customer ~s", [BillingId]),
  92
+            braintree_customer:create(BillingId)
  93
+    end.
  94
+
  95
+%%--------------------------------------------------------------------
  96
+%% @private
  97
+%% @doc
  98
+%%
  99
+%% @end
  100
+%%--------------------------------------------------------------------
  101
+-spec update_subscriptions/3 :: (ne_binary(), #wh_service_update{}, #wh_service_updates{}) -> #wh_service_updates{}.
  102
+update_subscriptions(PlanId, Subscription, #wh_service_updates{bt_subscriptions=Subscriptions}=Updates) ->
  103
+    Update = #wh_service_update{bt_subscription=Subscription, plan_id=PlanId},
  104
+    Updates#wh_service_updates{bt_subscriptions=lists:keystore(PlanId, #wh_service_update.plan_id, Subscriptions, Update)}.
  105
+
  106
+%%--------------------------------------------------------------------
  107
+%% @private
  108
+%% @doc
  109
+%%
  110
+%% @end
  111
+%%--------------------------------------------------------------------
  112
+-spec fetch_or_create_subscription/2 :: (ne_binary(), #wh_service_updates{}) -> braintree_subscription:subscription().
  113
+fetch_or_create_subscription(PlanId, #wh_service_updates{bt_subscriptions=Subscriptions
  114
+                                                         ,bt_customer=Customer}) ->
  115
+    case lists:keyfind(PlanId, #wh_service_update.plan_id, Subscriptions) of
  116
+        false ->
  117
+            try braintree_customer:get_subscription(PlanId, Customer) of
  118
+                Subscription -> 
  119
+                    lager:debug("found subscription ~s for plan id ~s"
  120
+                                ,[braintree_subscription:get_id(Subscription), PlanId]),
  121
+                    braintree_subscription:reset(Subscription)
  122
+            catch
  123
+                throw:{not_found, _} ->
  124
+                    lager:debug("creating new subscription for plan id ~s", [PlanId]),
  125
+                    braintree_customer:new_subscription(PlanId, Customer)
  126
+            end;
  127
+        #wh_service_update{bt_subscription=Subscription} -> Subscription
  128
+    end.                
  129
+
  130
+%%--------------------------------------------------------------------
  131
+%% @private
  132
+%% @doc
  133
+%%
  134
+%% @end
  135
+%%--------------------------------------------------------------------
  136
+-spec braintree_plan_addon_id/1 :: (wh_service_items:item()) -> {'undefined' | ne_binary(), 'undefined' | ne_binary()}.
  137
+braintree_plan_addon_id(ServiceItem) ->
  138
+    JObj = wh_service_item:bookkeeper(<<"braintree">>, ServiceItem),
  139
+    {wh_json:get_value(<<"plan">>, JObj), wh_json:get_value(<<"addon">>, JObj)}.
  140
+
  141
+%%--------------------------------------------------------------------
  142
+%% @private
  143
+%% @doc
  144
+%%
  145
+%% @end
  146
+%%--------------------------------------------------------------------
  147
+-spec braintree_cumulative_discount_id/1 :: (wh_service_items:item()) -> {'undefined' | ne_binary(), 'undefined' | ne_binary()}.
  148
+braintree_cumulative_discount_id(ServiceItem) ->
  149
+    wh_service_item:bookkeeper([<<"braintree">>, <<"discounts">>, <<"cumulative">>], ServiceItem).
  150
+
  151
+%%--------------------------------------------------------------------
  152
+%% @private
  153
+%% @doc
  154
+%%
  155
+%% @end
  156
+%%--------------------------------------------------------------------
  157
+-spec braintree_single_discount_id/1 :: (wh_service_items:item()) -> {'undefined' | ne_binary(), 'undefined' | ne_binary()}.
  158
+braintree_single_discount_id(ServiceItem) ->
  159
+    wh_service_item:bookkeeper([<<"braintree">>, <<"discounts">>, <<"single">>], ServiceItem).
23  lib/whistle_services-1.0.0/src/wh_service_invoice.erl
@@ -14,7 +14,7 @@
14 14
 -record(wh_invoice, {account_id = undefined
15 15
                      ,account_db = undefined
16 16
                      ,billing_id = undefined
17  
-                     ,items = wh_service_items:empty() :: wh_service_items:items()
  17
+                     ,service_items = wh_service_items:empty() :: wh_service_items:items()
18 18
                      ,service_plans = wh_service_plans:empty() :: wh_service_plans:plans()
19 19
                      ,services = wh_services:empty()
20 20
                     }).
@@ -22,17 +22,22 @@
22 22
 %%--------------------------------------------------------------------
23 23
 %% @public
24 24
 %% @doc
25  
-%%
  25
+%% Create a collection of billable items and envoke the bookkeeper
  26
+%% to actualize any billing changes.
26 27
 %% @end
27 28
 %%--------------------------------------------------------------------
28 29
 -spec sync/1 :: (ne_binary()) -> #wh_invoice{}.
29 30
 sync(Account) ->
30  
-    create(Account).
  31
+    #wh_invoice{billing_id=BillingId, service_items=ServiceItems} = create(Account),
  32
+    wh_bookkeeper_braintree:sync(ServiceItems, BillingId).
31 33
 
32 34
 %%--------------------------------------------------------------------
33 35
 %% @public
34 36
 %% @doc
35  
-%%
  37
+%% Create a new invoice for a given account.  An invoice is a collection
  38
+%% of service plans (provided by multiple vendors) applied to the
  39
+%% services the account is currently consuming to generate a collection
  40
+%% of items.
36 41
 %% @end
37 42
 %%--------------------------------------------------------------------
38 43
 -spec create/1 :: (ne_binary()) -> #wh_invoice{}.
@@ -40,9 +45,6 @@ create(Account) ->
40 45
     Routines = [fun(I) -> I#wh_invoice{account_id=wh_util:format_account_id(Account, raw)
41 46
                                        ,account_db=wh_util:format_account_id(Account, encoded)}
42 47
                 end
43  
-                ,fun(#wh_invoice{account_id=AccountId}=I) ->
44  
-                         I#wh_invoice{services=wh_services:fetch(AccountId)}
45  
-                 end
46 48
                 ,fun(#wh_invoice{account_db=AccountDb, account_id=AccountId}=I) ->
47 49
                          case couch_mgr:open_doc(AccountDb, AccountId) of
48 50
                              {ok, JObj} ->
@@ -55,10 +57,13 @@ create(Account) ->
55 57
                          end
56 58
                  end
57 59
                 ,fun(#wh_invoice{account_id=AccountId}=I) ->
58  
-                         I#wh_invoice{service_plans=wh_service_plans:fetch(AccountId)}
  60
+                         I#wh_invoice{services=wh_services:fetch(AccountId)}
  61
+                 end
  62
+                ,fun(#wh_invoice{billing_id=BillingId}=I) ->
  63
+                         I#wh_invoice{service_plans=wh_service_plans:fetch(BillingId)}
59 64
                  end
60 65
                 ,fun(#wh_invoice{service_plans=ServicePlans, services=Services}=I) ->
61  
-                         I#wh_invoice{items=wh_service_plans:create_items(Services, ServicePlans)}
  66
+                         I#wh_invoice{service_items=wh_service_plans:create_items(Services, ServicePlans)}
62 67
                  end
63 68
                ],
64 69
     lists:foldl(fun(F, I) -> F(I) end, #wh_invoice{}, Routines).
243  lib/whistle_services-1.0.0/src/wh_service_item.erl
... ...
@@ -0,0 +1,243 @@
  1
+%%%-------------------------------------------------------------------
  2
+%%% @copyright (C) 2012, VoIP, INC
  3
+%%% @doc
  4
+%%%
  5
+%%% @end
  6
+%%% @contributors
  7
+%%%-------------------------------------------------------------------
  8
+-module(wh_service_item).
  9
+
  10
+-export([empty/0]).
  11
+-export([set_category/2
  12
+         ,category/1
  13
+        ]).
  14
+-export([set_item/2
  15
+         ,item/1
  16
+        ]).
  17
+-export([set_quantity/2
  18
+         ,quantity/1
  19
+        ]).
  20
+-export([set_rate/2
  21
+         ,rate/1
  22
+        ]).
  23
+-export([set_single_discount/2
  24
+         ,single_discount/1
  25
+        ]).
  26
+-export([set_single_discount_rate/2
  27
+         ,single_discount_rate/1
  28
+        ]).
  29
+-export([set_cumulative_discount/2
  30
+         ,cumulative_discount/1
  31
+        ]).
  32
+-export([set_cumulative_discount_rate/2
  33
+         ,cumulative_discount_rate/1
  34
+        ]).
  35
+-export([set_bookkeepers/2]).
  36
+-export([bookkeeper/2]).
  37
+
  38
+-record(wh_service_item, {category = undefined
  39
+                          ,item = undefined
  40
+                          ,quantity = 0
  41
+                          ,rate = undefined
  42
+                          ,single_discount = false
  43
+                          ,single_discount_rate = 0.00
  44
+                          ,cumulative_discount = 0
  45
+                          ,cumulative_discount_rate = 0.00
  46
+                          ,bookkeepers = wh_json:new()
  47
+                         }).
  48
+
  49
+-type(item() :: #wh_service_item{}).
  50
+-export_type([item/0]).
  51
+
  52
+-include_lib("whistle_services/src/whistle_services.hrl").
  53
+
  54
+%%--------------------------------------------------------------------
  55
+%% @public
  56
+%% @doc
  57
+%% 
  58
+%% @end
  59
+%%--------------------------------------------------------------------
  60
+-spec empty() -> item().
  61
+empty() ->
  62
+    #wh_service_item{}.
  63
+
  64
+%%--------------------------------------------------------------------
  65
+%% @public
  66
+%% @doc
  67
+%% 
  68
+%% @end
  69
+%%--------------------------------------------------------------------
  70
+-spec category/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  71
+category(#wh_service_item{category=Category}) ->
  72
+    Category.
  73
+
  74
+%%--------------------------------------------------------------------
  75
+%% @public
  76
+%% @doc
  77
+%% 
  78
+%% @end
  79
+%%--------------------------------------------------------------------
  80
+-spec set_category/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  81
+set_category(Category, #wh_service_item{}=ServiceItem) ->
  82
+    ServiceItem#wh_service_item{category=Category}.
  83
+
  84
+
  85
+%%--------------------------------------------------------------------
  86
+%% @public
  87
+%% @doc
  88
+%% 
  89
+%% @end
  90
+%%--------------------------------------------------------------------
  91
+-spec item/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  92
+item(#wh_service_item{item=Item}) ->
  93
+    Item.
  94
+
  95
+%%--------------------------------------------------------------------
  96
+%% @public
  97
+%% @doc
  98
+%% 
  99
+%% @end
  100
+%%--------------------------------------------------------------------
  101
+-spec set_item/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  102
+set_item(Item, #wh_service_item{}=ServiceItem) ->
  103
+    ServiceItem#wh_service_item{item=Item}.
  104
+
  105
+%%--------------------------------------------------------------------
  106
+%% @public
  107
+%% @doc
  108
+%% 
  109
+%% @end
  110
+%%--------------------------------------------------------------------
  111
+-spec quantity/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  112
+quantity(#wh_service_item{quantity=Quantity}) ->
  113
+    Quantity.
  114
+
  115
+%%--------------------------------------------------------------------
  116
+%% @public
  117
+%% @doc
  118
+%% 
  119
+%% @end
  120
+%%--------------------------------------------------------------------
  121
+-spec set_quantity/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  122
+set_quantity(Quantity, #wh_service_item{}=ServiceItem) ->
  123
+    ServiceItem#wh_service_item{quantity=Quantity}.
  124
+
  125
+%%--------------------------------------------------------------------
  126
+%% @public
  127
+%% @doc
  128
+%% 
  129
+%% @end
  130
+%%--------------------------------------------------------------------
  131
+-spec rate/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  132
+rate(#wh_service_item{rate=Rate}) ->
  133
+    Rate.
  134
+
  135
+%%--------------------------------------------------------------------
  136
+%% @public
  137
+%% @doc
  138
+%% 
  139
+%% @end
  140
+%%--------------------------------------------------------------------
  141
+-spec set_rate/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  142
+set_rate(Rate, #wh_service_item{}=ServiceItem) ->
  143
+    ServiceItem#wh_service_item{rate=Rate}.
  144
+
  145
+%%--------------------------------------------------------------------
  146
+%% @public
  147
+%% @doc
  148
+%% 
  149
+%% @end
  150
+%%--------------------------------------------------------------------
  151
+-spec single_discount/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  152
+single_discount(#wh_service_item{single_discount=SingleDiscount}) ->
  153
+    SingleDiscount.
  154
+
  155
+%%--------------------------------------------------------------------
  156
+%% @public
  157
+%% @doc
  158
+%% 
  159
+%% @end
  160
+%%--------------------------------------------------------------------
  161
+-spec set_single_discount/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  162
+set_single_discount(SingleDiscount, #wh_service_item{}=ServiceItem) ->
  163
+    ServiceItem#wh_service_item{single_discount=SingleDiscount}.
  164
+
  165
+%%--------------------------------------------------------------------
  166
+%% @public
  167
+%% @doc
  168
+%% 
  169
+%% @end
  170
+%%--------------------------------------------------------------------
  171
+-spec single_discount_rate/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  172
+single_discount_rate(#wh_service_item{single_discount_rate=Rate}) ->
  173
+    Rate.
  174
+
  175
+%%--------------------------------------------------------------------
  176
+%% @public
  177
+%% @doc
  178
+%% 
  179
+%% @end
  180
+%%--------------------------------------------------------------------
  181
+-spec set_single_discount_rate/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  182
+set_single_discount_rate(Rate, #wh_service_item{}=ServiceItem) ->
  183
+    ServiceItem#wh_service_item{single_discount_rate=Rate}.
  184
+
  185
+%%--------------------------------------------------------------------
  186
+%% @public
  187
+%% @doc
  188
+%% 
  189
+%% @end
  190
+%%--------------------------------------------------------------------
  191
+-spec cumulative_discount/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  192
+cumulative_discount(#wh_service_item{cumulative_discount=Quantity}) ->
  193
+    Quantity.
  194
+
  195
+%%--------------------------------------------------------------------
  196
+%% @public
  197
+%% @doc
  198
+%% 
  199
+%% @end
  200
+%%--------------------------------------------------------------------
  201
+-spec set_cumulative_discount/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  202
+set_cumulative_discount(Quantity, #wh_service_item{}=ServiceItem) ->
  203
+    ServiceItem#wh_service_item{cumulative_discount=Quantity}.
  204
+
  205
+%%--------------------------------------------------------------------
  206
+%% @public
  207
+%% @doc
  208
+%% 
  209
+%% @end
  210
+%%--------------------------------------------------------------------
  211
+-spec cumulative_discount_rate/1 :: (#wh_service_item{}) -> 'undefined' | ne_binary().
  212
+cumulative_discount_rate(#wh_service_item{cumulative_discount_rate=Rate}) ->
  213
+    Rate.
  214
+
  215
+%%--------------------------------------------------------------------
  216
+%% @public
  217
+%% @doc
  218
+%% 
  219
+%% @end
  220
+%%--------------------------------------------------------------------
  221
+-spec set_cumulative_discount_rate/2 :: (ne_binary(), #wh_service_item{}) -> #wh_service_item{}.
  222
+set_cumulative_discount_rate(Rate, #wh_service_item{}=ServiceItem) ->
  223
+    ServiceItem#wh_service_item{cumulative_discount_rate=Rate}.
  224
+
  225
+%%--------------------------------------------------------------------
  226
+%% @public
  227
+%% @doc
  228
+%% 
  229
+%% @end
  230
+%%--------------------------------------------------------------------
  231
+-spec bookkeeper/2 :: (ne_binary(), #wh_service_item{}) -> 'undefined' | term().
  232
+bookkeeper(Bookkeeper, #wh_service_item{bookkeepers=Bookkeepers}) ->
  233
+    wh_json:get_ne_value(Bookkeeper, Bookkeepers).
  234
+
  235
+%%--------------------------------------------------------------------
  236
+%% @public
  237
+%% @doc
  238
+%% 
  239
+%% @end
  240
+%%--------------------------------------------------------------------
  241
+-spec set_bookkeepers/2 :: (wh_json:json_object(), #wh_service_item{}) -> #wh_service_item{}.
  242
+set_bookkeepers(Bookkeepers, #wh_service_item{}=ServiceItem) ->
  243
+    ServiceItem#wh_service_item{bookkeepers=Bookkeepers}.
102  lib/whistle_services-1.0.0/src/wh_service_items.erl
@@ -8,27 +8,15 @@
8 8
 -module(wh_service_items).
9 9
 
10 10
 -export([empty/0]).
11  
--export([update/5]).
12  
--export([set_single_discount/4]).
13  
--export([set_cumulative_discount/5]).
  11
+-export([to_list/1]).
  12
+-export([find/3]).
  13
+-export([update/2]).
14 14
 
15  
--include_lib("whistle_services/src/whistle_services.hrl").
16  
-
17  
--record(wh_service_item, {category = undefined
18  
-                          ,item = undefined
19  
-                          ,quantity = 0
20  
-                          ,rate = undefined
21  
-                          ,single_discount = false
22  
-                          ,single_discount_rate = 0.00
23  
-                          ,cumulative_discount = 0
24  
-                          ,cumulative_discount_rate = 0.00
25  
-                         }).
26  
--type(item() :: #wh_service_item{}).
27  
--type(items() :: [item(),...] | []).
28  
-
29  
--export_type([item/0]).
  15
+-type(items() :: dict:new()).
30 16
 -export_type([items/0]).
31 17
 
  18
+-include_lib("whistle_services/src/whistle_services.hrl").
  19
+
32 20
 %%--------------------------------------------------------------------
33 21
 %% @public
34 22
 %% @doc
@@ -45,16 +33,9 @@ empty() ->
45 33
 %% 
46 34
 %% @end
47 35
 %%--------------------------------------------------------------------
48  
--spec update/5 :: (ne_binary(), ne_binary(), integer(), 'undefined' | float(), items()) -> items().
49  
-update(Category, Item, Quantity, Rate, Items) ->
50  
-    case wh_util:is_empty(Rate) of
51  
-        true -> lager:debug("set item '~s/~s' quantity ~p @ default rate", [Category, Item, Quantity]);
52  
-        false -> lager:debug("set item '~s/~s' quantity ~p @ $~p", [Category, Item, Quantity, Rate])
53  
-    end,
54  
-    Key = {Category, Item},
55  
-    dict:update(Key, fun(#wh_service_item{}=ExistingItem) ->
56  
-                             ExistingItem#wh_service_item{quantity=Quantity, rate=Rate}
57  
-                     end, #wh_service_item{quantity=Quantity, rate=Rate, category=Category, item=Item}, Items).    
  36
+-spec to_list/1 :: (items()) -> [wh_service_item:item(),...] | [].
  37
+to_list(ServiceItems) ->
  38
+    [ServiceItem || {_, ServiceItem} <- dict:to_list(ServiceItems)].
58 39
 
59 40
 %%--------------------------------------------------------------------
60 41
 %% @public
@@ -62,17 +43,13 @@ update(Category, Item, Quantity, Rate, Items) ->
62 43
 %% 
63 44
 %% @end
64 45
 %%--------------------------------------------------------------------
65  
--spec set_single_discount/4 :: (ne_binary(), ne_binary(), 'undefined' | float(), items()) -> items().
66  
-set_single_discount(Category, Item, Rate, Items) ->
67  
-    case wh_util:is_empty(Rate) of
68  
-        true -> lager:debug("set item '~s/~s' single discount @ default rate", [Category, Item]);
69  
-        false -> lager:debug("set item '~s/~s' single discount @ $~p", [Category, Item, Rate])
70  
-    end,
  46
+-spec find/3 :: (ne_binary(), ne_binary(), items()) -> wh_service_item:item().
  47
+find(Category, Item, ServiceItems) ->
71 48
     Key = {Category, Item},
72  
-    dict:update(Key, fun(#wh_service_item{}=ExistingItem) ->
73  
-                             ExistingItem#wh_service_item{single_discount=true, single_discount_rate=Rate}
74  
-                     end, #wh_service_item{single_discount=true, single_discount_rate=Rate
75  
-                                           ,category=Category, item=Item}, Items).
  49
+    case dict:find(Key, ServiceItems) of
  50
+        {ok, I} -> I;
  51
+        error -> wh_service_item:empty()
  52
+    end.
76 53
 
77 54
 %%--------------------------------------------------------------------
78 55
 %% @public
@@ -80,14 +57,41 @@ set_single_discount(Category, Item, Rate, Items) ->
80 57
 %% 
81 58
 %% @end
82 59
 %%--------------------------------------------------------------------
83  
--spec set_cumulative_discount/5 :: (ne_binary(), ne_binary(), integer(), 'undefined' | float(), items()) -> items().
84  
-set_cumulative_discount(Category, Item, Quantity, Rate, Items) ->
85  
-    case wh_util:is_empty(Rate) of
86  
-        true -> lager:debug("set item '~s/~s' cumulative discount quantity ~p @ default rate", [Category, Item, Quantity]);
87  
-        false -> lager:debug("set item '~s/~s' cumulative discount quantity ~p @ $~p", [Category, Item, Quantity, Rate])
88  
-    end,
89  
-    Key = {Category, Item},
90  
-    dict:update(Key, fun(#wh_service_item{}=ExistingItem) ->
91  
-                             ExistingItem#wh_service_item{cumulative_discount=Quantity, cumulative_discount_rate=Rate}
92  
-                     end, #wh_service_item{cumulative_discount=Quantity, cumulative_discount_rate=Rate
93  
-                                           ,category=Category, item=Item}, Items).
  60
+-spec update/2 :: (wh_service_item:item(), items()) -> wh_service_item:items().
  61
+update(ServiceItem, ServiceItems) ->
  62
+    Category = wh_service_item:category(ServiceItem),
  63
+    Item = wh_service_item:item(ServiceItem),
  64
+    _ = case wh_service_item:rate(ServiceItem) of
  65
+            undefined ->
  66
+                lager:debug("set '~s/~s' with quantity ~p @ default rate"
  67
+                            ,[Category, Item, wh_service_item:quantity(ServiceItem)]);
  68
+            Rate ->
  69
+                lager:debug("set '~s/~s' with quantity ~p @ $~p"
  70
+                            ,[Category, Item, wh_service_item:quantity(ServiceItem), Rate])
  71
+        end,
  72
+    CumulativeDiscount = wh_service_item:cumulative_discount(ServiceItem),
  73
+    _ = case wh_util:is_empty(CumulativeDiscount) 
  74
+            orelse wh_service_item:cumulative_discount_rate(ServiceItem) 
  75
+        of
  76
+            true -> ok;
  77
+            undefined ->
  78
+                lager:debug("set '~s/~s' cumulative discount with quantity ~p @ default rate"
  79
+                            ,[Category, Item, CumulativeDiscount]);
  80
+            CumulativeRate ->
  81
+                lager:debug("set '~s/~s' cumulative discount with quantity ~p @ $~p"
  82
+                            ,[Category, Item, CumulativeDiscount, CumulativeRate])
  83
+
  84
+        end,
  85
+    _ = case wh_service_item:single_discount(ServiceItem) 
  86
+            andalso wh_service_item:single_discount_rate(ServiceItem)
  87
+        of
  88
+            false -> ok;
  89
+            undefined -> 
  90
+                lager:debug("set '~s/~s' single discount at default rate"
  91
+                            ,[Category, Item]);
  92
+            SingleRate ->
  93
+                lager:debug("set '~s/~s' single discount for $~p"
  94
+                            ,[Category, Item, SingleRate])
  95
+        end,
  96
+    Key = {wh_service_item:category(ServiceItem), wh_service_item:item(ServiceItem)},
  97
+    dict:store(Key, ServiceItem, ServiceItems).
81  lib/whistle_services-1.0.0/src/wh_service_plan.erl
@@ -15,15 +15,16 @@
15 15
 %%--------------------------------------------------------------------
16 16
 %% @public
17 17
 %% @doc
18  
-%%
  18
+%% Given a vendor database and service plan id, fetch the document.
  19
+%% Merge any plan overrides into the plan property.
19 20
 %% @end
20 21
 %%--------------------------------------------------------------------
21 22
 -spec fetch/3 :: (ne_binary(), ne_binary(), wh_json:json_object()) -> 'undefined' | wh_json:json_object().
22  
-fetch(PlanId, VendorDb, _Overrides) ->
  23
+fetch(PlanId, VendorDb, Overrides) ->
23 24
     case couch_mgr:open_doc(VendorDb, PlanId) of
24 25
         {ok, JObj} -> 
25 26
             lager:debug("using service plan ~s in vendor db ~s", [PlanId, VendorDb]),
26  
-            JObj;
  27
+            wh_json:merge_recursive(JObj, wh_json:from_list([{<<"plan">>, Overrides}]));
27 28
         {error, _R} ->
28 29
             lager:debug("unable to open service plan ~s in vendor db ~s: ~p", [PlanId, VendorDb, _R]),
29 30
             undefined
@@ -38,37 +39,41 @@ fetch(PlanId, VendorDb, _Overrides) ->
38 39
 -spec create_items/3 :: (wh_json:json_object(), wh_service_items:items(), wh_services:services()) -> wh_service_items:items().
39 40
 -spec create_items/5 :: (ne_binary(), ne_binary(), wh_json:json_object(), wh_service_items:items(), wh_services:services()) -> wh_service_items:items().
40 41
 
41  
-create_items(ServicePlan, Items, Services) ->
  42
+create_items(ServicePlan, ServiceItems, Services) ->
42 43
     Plan = wh_json:get_value(<<"plan">>, ServicePlan, wh_json:new()),
43 44
     Plans = [{Category, Item}
44 45
              || Category <- wh_json:get_keys(Plan)
45 46
                     ,Item <- wh_json:get_keys(Category, Plan)
46 47
             ],