More annoying.
[wolnelektury.git] / src / club / payu / models.py
1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 import json
5 from urllib.parse import urlencode
6 from urllib.request import HTTPError
7 from django.contrib.sites.models import Site
8 from django.db import models
9 from django.urls import reverse
10 from django.utils.translation import ugettext_lazy as _
11 from . import POSS
12
13
14 class CardToken(models.Model):
15     """ This should be attached to a payment schedule. """
16     pos_id = models.CharField(_('POS id'), max_length=255)
17     disposable_token = models.CharField(_('disposable token'), max_length=255)
18     reusable_token = models.CharField(_('reusable token'), max_length=255, null=True, blank=True)
19     created_at = models.DateTimeField(_('created_at'), auto_now_add=True)
20
21     class Meta:
22         abstract = True
23         verbose_name = _('PayU card token')
24         verbose_name_plural = _('PayU card tokens')
25
26
27 class Order(models.Model):
28     pos_id = models.CharField(_('POS id'), max_length=255)   # TODO: redundant?
29     customer_ip = models.GenericIPAddressField(_('customer IP'))
30     order_id = models.CharField(_('order ID'), max_length=255, blank=True)
31
32     status = models.CharField(max_length=128, blank=True, choices=[
33         ('PENDING', _('Pending')),
34         ('WAITING_FOR_CONFIRMATION', _('Waiting for confirmation')),
35         ('COMPLETED', _('Completed')),
36         ('CANCELED', _('Canceled')),
37         ('REJECTED', _('Rejected')),
38     ])
39
40     class Meta:
41         abstract = True
42         verbose_name = _('PayU order')
43         verbose_name_plural = _('PayU orders')
44
45     # These need to be provided in a subclass.
46
47     def get_amount(self):
48         raise NotImplementedError()
49
50     def get_buyer(self):
51         raise NotImplementedError()
52
53     def get_continue_url(self):
54         raise NotImplementedError()
55     
56     def get_description(self):
57         raise NotImplementedError()
58
59     def get_products(self):
60         """ At least: name, unitPrice, quantity. """
61         return [
62             {
63                 'name': self.get_description(),
64                 'unitPrice': str(int(self.get_amount() * 100)),
65                 'quantity': '1',
66             },
67         ]
68
69     def is_recurring(self):
70         return False
71
72     def get_notify_url(self):
73         raise NotImplementedError
74
75     def status_updated(self):
76         pass
77
78     # 
79     
80     def get_pos(self):
81         return POSS[self.pos_id]
82
83     def get_representation(self, token=None):
84         rep = {
85             "notifyUrl": self.get_notify_url(),
86             "customerIp": self.customer_ip,
87             "merchantPosId": self.pos_id,
88             "currencyCode": self.get_pos().currency_code,
89             "totalAmount": str(int(self.get_amount() * 100)),
90             "extOrderId": "wolne-lektury-rcz-%d" % self.pk,
91
92             "buyer": self.get_buyer() or {},
93             "continueUrl": self.get_continue_url(),
94             "description": self.get_description(),
95             "products": self.get_products(),
96         }
97         if token:
98             token = self.get_card_token()
99             rep['recurring'] = 'STANDARD' if token.reusable_token else 'FIRST'
100             rep['payMethods'] = {
101                 "payMethod": {
102                     "type": "CARD_TOKEN",
103                     "value": token.reusable_token or token.disposable_token
104                 }
105             }
106         return rep
107
108     def put(self):
109         token = self.get_card_token() if self.is_recurring() else None
110         representation = self.get_representation(token)
111         try:
112             response = self.get_pos().request('POST', 'orders', representation)
113         except HTTPError as e:
114             resp = json.loads(e.read().decode('utf-8'))
115             if resp['status']['statusCode'] == 'ERROR_ORDER_NOT_UNIQUE':
116                 pass
117             else:
118                 raise
119
120         if token:
121             reusable_token = response.get('payMethods', {}).get('payMethod', {}).get('value', None)
122             if reusable_token:
123                 token.reusable_token = reusable_token
124                 token.save()
125             # else?
126
127         self.order_id = response['orderId']
128         self.save()
129
130         
131         return response.get('redirectUri', self.schedule.get_thanks_url())
132
133
134 class Notification(models.Model):
135     """ Add `order` FK to real Order model. """
136     body = models.TextField(_('body'))
137     received_at = models.DateTimeField(_('received_at'), auto_now_add=True)
138
139     class Meta:
140         abstract = True
141         verbose_name = _('PayU notification')
142         verbose_name_plural = _('PayU notifications')
143
144     def get_status(self):
145         return json.loads(self.body)['order']['status']
146
147     def apply(self):
148         status = self.get_status()
149         if self.order.status not in (status, 'COMPLETED'):
150             self.order.status = status
151             self.order.save()
152             self.order.status_updated()