diff --git a/src/Domain.py b/src/Domain.py index c99a7bb..93c5307 100644 --- a/src/Domain.py +++ b/src/Domain.py @@ -17,7 +17,11 @@ class User: self.stripe = stripe; self.stripeToken= stripeToken self.user = None + def initialize(self,uid) : + id = self.getId(uid) + if self.db.doc_exist(id) : + self.user = self.db.get(id) """ This function will cast an object to JSON, It is designed to address an inherent limitation of stripe object hierarchy @@ -32,14 +36,17 @@ class User: @param plans list of plans the user is subscribing """ def getId(self,uid): - r = self.db.view('federation/ids',key=uid) + r = self.db.view('federation/uid_map',key=uid) + if r.count() > 0 : r = r.first() return r['value'] else: None + def init (self,uid,plans): customer = {} + id = self.getId(uid) if id is not None: self.user = self.db.get(id) @@ -50,9 +57,11 @@ class User: if 'sources' not in self.user or self.user['sources'] is None: #@TODO: get the appropriate id - customer = stripe.Customer.retrieve(self.user['id']) - customer.source=self.stripeToken - customer.save() + # customer = stripe.Customer.retrieve(self.user['id']) + #customer = stripe.Customer.retrieve(id) + #customer.source=self.stripeToken + #customer.save() + pass # self.hasPlan(uid,plan) has_plan = True if self.user is None and plans and stripe : @@ -65,18 +74,23 @@ class User: email=uid ) ; - + # + # The customer is being persisted on our end + # We do this considering user federations i.e several emails attached to an account + # has_plan = False id = customer['id'] - self.user = {"_id":uid,"id":id,"source":self.stripeToken} + #self.user = {"_id":uid,"id":id,"source":self.stripeToken} + self.user = {"_id":id,"emails":[uid],"source":self.stripeToken} else: # # The user exists but let's see if the user is subscribed to this plan # If she is and the plan is still live then there is nothing to do # - self.user = self.db.get(uid) ; - id = self.user['id'] + #self.user = self.db.get(uid) ; + self.user = self.db.get(id) + # id = self.user['id'] # # If this user's isn't already subscribed we should subscribe her # We perform a set operation to determine if she is alread susbscribed @@ -109,12 +123,17 @@ class User: # # call stripe to retrieve subscriptions for this user ... # - if self.user : - r = stripe.Customer.retrieve(self.user['id']) + + if self.user is not None : + id = self.user['_id'] + r = stripe.Customer.retrieve(id) return r.subscriptions else: return [] - + def plans(self): + lsub = self.subscriptions() + plans = [ sub['plan'] for sub in lsub if sub['ended_at'] is None ] + return plans """ This function subscribes a customer to n-plans @@ -129,10 +148,12 @@ class User: plan=plan['id'] ) ; else: - - customer = stripe.Customer.retrieve(id) - - self.user['sources'] = self.cast(customer.sources) + # + # The provided token has been updated on the customer's profile (init function) + # Because of this we can just subscribe her and the card will be used accordingly + # + customer = stripe.Customer.retrieve(id) + self.user['sources'] = self.cast(customer.sources) sub = self.stripe.Subscription.create( customer=id, plan=plan['id'], @@ -146,30 +167,10 @@ class User: # #r.append(x) return r - """ - This function will save card information - """ - def save_card(self,uid,token): - user = self.db.get(uid) - if 'sources' not in user or (token not in user['sources']): - # - # In this case we don't have a card - # - id = user['id'] - customer = stripe.Customer.retrieve(id) - card = customer.sources.create(source = token) - if 'sources' not in user: - user['sources'] = [] - - user['sources'].append(card['id']) - - self.db.save_doc(user) ; - self.user = user - # - # Creating an invoice for this user def invoice(self,uid,sid): - self.user = self.db.get(uid) - id = self.user['id'] + #self.user = self.db.get(uid) + #id = self.user['id'] + id = self.getId(uid) ; r = [ stripe.InvoiceItem.create(customer=id,amount=item['amount'],currency=item['currency'],description=item['statement_descriptor'],subscription=item['subscription'],invoice=iid) for item in plans] return r """ @@ -177,9 +178,10 @@ class User: @pre : an item with amount > 0 and status past_due """ def get_invoices(self,uid): - info = self.db.get(uid) - id = info['id'] - #return stripe.Invoice.list(customer=id) + #info = self.db.get(uid) + #id = info['id'] + id = self.getId(uid) + return stripe.Invoice.list(customer=id) """ This function will clear a particular invoice (hopefully). No partial payments should be accepted @@ -187,8 +189,9 @@ class User: """ def charge(self,uid,token,plans): - info = self.db.get(uid) - id = info['id'] + #info = self.db.get(uid) + #id = info['id'] + id = self.getId(uid) sid = plans[0]['subscription'] user = stripe.Customer.retrieve(id) user.source = token @@ -221,17 +224,13 @@ class User: else: #perform an update pass - """ - This function updates/creates a user remotely - @pre: - """ - def publish(self,info={}): - # We need to figure out whether to create or update; - # - if self.user is not None and info is not None: - customer = self.stripe.Customer.retrieve(self.user['id']) ; - customer.metadata = info ; - customer.save() ; + def update_user(self,**args) : + id = args['id'] + uid= args['uid'] + self.user = self.db.get(id) + self.user['emails'].append(uid) + self.user['emails'] = list(set(self.user['emails'])) + self.db.save_doc(self.user) #f = open('config.json') #conf = json.loads(f.read()) #f.close() diff --git a/src/api/index.py b/src/api/index.py index f56de56..aa9bf18 100644 --- a/src/api/index.py +++ b/src/api/index.py @@ -28,11 +28,39 @@ stripe.api_key = stripe_keys['secret_key'].strip() app = Flask(__name__) +COUCHDB = Server(uri=CONFIG['couchdb']['uri']) ; """ - This function will attempt to create an account for a user if the user does NOT exist - Then Setup the stage for initialization of {signup for, user-account} + This function will set the user information to the session and update the information + @header uid user email address """ -COUCHDB = Server(uri=CONFIG['couchdb']['uri']) ; +@app.route("/init/",methods=['POST']) +def init(app_name): + + if 'uid' in request.headers: + uid = request.headers['uid'] + couchdb = Couchdb(uri=CONFIG['couchdb']['uri'],dbname=app_name,uid=None) + DB = couchdb.dbase + handler = Domain.User(DB,stripe) ; + handler.init(uid,None) + + id=handler.getId(uid) + session['customer.id'] = id + + return ('',204) +""" + This function will update the user's email +""" +@app.route('/init/',methods=['PUT']) +def init_update(app_name): + uid = request.headers['uid'] + id = session['customer.id'] + couchdb = Couchdb(uri=CONFIG['couchdb']['uri'],dbname=app_name,uid=None) + DB = couchdb.dbase + handler = Domain.User(DB,stripe) + handler.update_user(uid=uid,id=id) + + return ('',204) + """ This function subscribes a user to a given service for an application @@ -51,76 +79,68 @@ def subscribe(app_name): uid = request.form['stripeEmail'] tokenType = request.form['stripeTokenType'] amount = request.form['amount'] - key = request.form['plan'] + pid = request.form['plan'] else: - key = request.headers['key'] + pid = request.headers['pid'] uid = request.headers['uid'] stripeToken = None + plans = stripe.Plan.list().data - plan = [item for item in plans if item.id == key] + plan = [item for item in plans if item.id == pid] resp = "0" + couch_handler = Couchdb(uri=CONFIG['couchdb']['uri'],dbname=app_name,uid=uid) DB = couch_handler.dbase handler = Domain.User(DB,stripe,stripeToken) ; if plan : handler.init(uid,plan) - - resp = plan[0].id - return ('done',204) + return ('',204) + """ - This function returns the meta data about a given plan or set of plans for a given application - @resource app_name application identifier - @header pid plan identifier (optional) - @header uid user identifier + This function returns the features of a user for a given application + The features are returned provided the plan is still valid i.e (end_date,status={active|trailing}) + @header uid user's email """ -@app.route('/get/info/',methods=['GET']) -def get_plan_info(app_name) : +@app.route('/features/') +def features(app_name): plans = [] - print ' *** ','uid' in request.headers if 'uid' in request.headers : uid = request.headers['uid'] - pid = request.headers['pid'] if 'pid' in request.headers else None + couchdb = CouchdbReader(uri=CONFIG['couchdb']['uri'],dbname=app_name,uid=uid,create=False) - info = couchdb.read() - lsub = info['subscriptions'] + handler = Domain.User(couchdb.dbase,stripe) + handler.initialize(uid) + lsub = handler.subscriptions() plans = [ sub['plan'] for sub in lsub if sub['ended_at'] is None ] - if pid is not None : - plans = [item['metadata'] for item in plans if item['id'] == pid] - else: - # - # This function returns the plans for a given application - # We assume the application name is the prefix of the plan identifier in stripe - # - plans = stripe.Plan.list(limit=10) - handler = Domain.User(None) - plans = handler.cast(plans.data) - - plans = [item for item in plans if re.match(app_name,item['name'])] + plans = [item['metadata'] for item in plans if item['id']] + return json.dumps(plans) -@app.route('/get/sub/') -def get_sub_info(app_name): +""" + This function returns a user's plans/status for an application + @header uid user's email address +""" +@app.route('/status/') +def status(app_name): + uid = request.headers['uid'] - pid = request.headers['pid'] if 'pid' in request.headers else None + couchdb = CouchdbReader(uri=CONFIG['couchdb']['uri'],dbname=app_name,uid=uid,create=False) - info = couchdb.read() - lsub = info['subscriptions'] - # - # @TODO: Return critical information only i.e: - # - subscription state (dates,status) - # - how much is owed - # - subscription id - # - if pid is not None: - subs = [ sub for sub in lsub if sub['plan']['id'] == pid ] - else: - subs = lsub - return json.dumps(subs) -@app.route('/get/plans/') + handler = Domain.User(couchdb.dbase,stripe) + handler.initialize(uid) + #lsub = handler.subscriptions() + plans = handler.plans() + return json.dumps(plans) +""" + This endpoint is the signup form for a given application, + It will allow user's to be able to signup for various plans + @pre 'uid' in request.headers +""" +@app.route('/signup/') def get_plans(app_name) : apikey = CONFIG['stripe']['pub'].strip() # @@ -129,10 +149,22 @@ def get_plans(app_name) : # uid = session['uid'] if 'uid' in session else 'nyemba@gmail.com' plans = stripe.Plan.list(limit=10) - handler = Domain.User(None) - plans = handler.cast(plans.data) - + + # plans = handler.cast(plans.data) + plans = json.loads(json.dumps(plans.data, sort_keys=True, indent=2)) + plans = [item for item in plans if re.match(app_name,item['name'])] + if uid is not None: + couchdb = Couchdb(uri=CONFIG['couchdb']['uri'],dbname=app_name,uid=uid,create=False) + handler = Domain.User(couchdb.dbase,stripe) + + handler.init(uid,[]) + subs = handler.subscriptions().data + ids = [item.id for item in subs] + + # + # @TODO: Mark the plans the current user is signed up for + # return render_template('subscribe.html',uid=uid,app_name=app_name,plans=plans,apikey=apikey) @app.route('/subscribe/',methods=['DELETE']) @@ -191,132 +223,6 @@ def is_customer (app_name): html = html.replace(":email",uid).replace(":amount",str(amount)).replace(":key",apikey).replace(":app_name",app_name) amount = amount / 100 return render_template('bill.html',apikey=apikey,app_name=app_name.replace('-',' '),plans=plans,total_amount=amount,html=html) -""" - This function is intended to performa an actual payment -""" -@app.route('/pay/',methods=['POST']) -def pay(app_name): - - token = request.form['stripeToken'] - uid = request.form['stripeEmail'] - tokenType = request.form['stripeTokenType'] - couchdb = CouchdbReader(uri=CONFIG['couchdb']['uri'],dbname=app_name,uid=uid,create=False) - - DB = couchdb.dbase #COUCHDB.get_db(CONFIG['couchdb']['db']) ; - handler = Domain.User(DB,stripe) ; - plans = session['plans'] - # Assuming all is fine, we must do the following at this point - # - create an invoice with the designated subscriptions - # - create a charge on the invoice - # - #items = handler.invoice(uid,plans) - - handler.charge(uid,token,plans) - # Let's insure the preconditions are met i.e - # card,invoice - #info = session['user-info'] - #uid = info['_id'] - #if 'sources' not in info or token not in info['sources']: - # - # Let's create the card - # handler.save_card(uid,token) - # - # Let's create a charge here ... - #plans = session['user-plans'] - - #amount=[item['price'] for item in plans if item['status'] == 'past_due' ] - #if len(amount) == 0: - # amount = 0 - #else: - # amount = sum(amount) - #handler.charge(uid,amount) - session['user-info'] = handler.user - return ('',204) - #return render_template("bill.html",plans=plans, total_amount=amount) - - -@app.route('/bill',methods=['GET']) -def bill(): - return render_template('bill.html') -@app.route('/buy',methods=['POST']) -def buy(): - id = request.form['id'] ; - - if id in ['subscribe','now']: - email = request.form['stripeEmail'] - token = request.form['stripeToken'] - user = Domain.User(DB,stripe,token) ; - user.init(email) ; - if user.exists() : - print "Update with anything if need be" - pass - else: - # - # Create the user with the associated plan/or payment method - # - user.save() ; - user.publish() - else: - pass - return "0" -""" - This function provides general information about service plans - @TODO: provide filtering criteria -""" -@app.route('/plans',methods=['GET']) -def plans(): - plans = stripe.Plan.list().data - if 'filter' in request.headers: - filter = request.headers['filter'] - - plans = [ item for item in plans if re.match(filter,item.name)] - else: - # - # Let's get a user's subscription information - # - uid = request.headers['uid'] - - if 'uid' in request.headers and request.headers['uid'] != '': - uid = request.headers['uid'] - DB = COUCHDB.get_db(CONFIG['couchdb']['db']) ; - handler = Domain.User(DB) - handler.init(uid) - myplans = [{"id":item.plan.id,"price":item.plan.amount/100,"feature":item.plan.metadata['info'],"status":item.status} for item in handler.subscriptions()] - keys = [plan['id'] for plan in myplans] - plans = [plan for plan in plans if plan['price'] > 0 and plan['id'] not in keys] - plans = {'myplans':myplans,"plans":plans} - session['user-plans'] = myplans - plans = json.dumps(plans) - return plans -""" - This function subscribes a user to a given plan(s) - If the plan is new, then we do NOT need a credit card info - - @header app application/requesting service - @body info {user:_id,plan:[]} -""" -@app.route('/_subscribe',methods=['POST']) -def _subscribe(): - - - - - if 'user-info' not in session: - info = request.get_json(silent=True) - user = info['user'] - plans = info['plans'] - else: - plans = [{"id":id} for id in request.get_json(silent=True)] - user = session['user-info'] - app = request.headers['app'] - # - # @TODO: - # This should be handled by transport layer ... - # - DB = COUCHDB.get_db(CONFIG['couchdb']['db']) ; - handler = Domain.User(DB,stripe) - r = handler.subscribe(user['id'],plans) - return json.dumps(r) if __name__ == '__main__' : app.debug = True ;