diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index 3be3c2c..52e5281 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -10,6 +10,7 @@ from calendar import monthrange import uuid +# Define DecimalField properties, used to represent amounts of money. AMOUNT_MAX_DIGITS=10 AMOUNT_DECIMALS=2 @@ -23,6 +24,70 @@ class RecurringPeriod(models.TextChoices): PER_HOUR = 'HOUR', _('Per Hour') PER_SECOND = 'SECOND', _('Per Second') +### +# Payments and Payment Methods. + +class Payment(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE) + + amount = models.DecimalField( + default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + + source = models.CharField(max_length=256, + choices = ( + ('wire', 'Wire Transfer'), + ('stripe', 'Stripe'), + ('voucher', 'Voucher'), + ('referral', 'Referral'), + ('unknown', 'Unknown') + ), + default='unknown') + timestamp = models.DateTimeField(editable=False, auto_now_add=True) + +class PaymentMethod(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE, + editable=False) + source = models.CharField(max_length=256, + choices = ( + ('stripe', 'Stripe'), + ('unknown', 'Unknown'), + ), + default='stripe') + description = models.TextField() + primary = models.BooleanField(default=True) + + # Only used for "Stripe" source + stripe_card_id = models.CharField(max_length=32, blank=True, null=True) + + def charge(self, amount): + if amount > 0: # Make sure we don't charge negative amount by errors... + if self.source == 'stripe': + # TODO: wire to stripe, see meooow-payv1/strip_utils.py + payment = Payment(owner=self.owner, source=self.source, amount=amount) + payment.save() # TODO: Check return status + + return True + else: + # We do not handle that source yet. + return False + else: + return False + + class Meta: + unique_together = [['owner', 'primary']] + + +### +# Bills & Payments. + class Bill(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) owner = models.ForeignKey(get_user_model(), @@ -56,6 +121,10 @@ class Bill(models.Model): return self.ending_date < timezone.now() class BillRecord(): + """ + Entry of a bill, dynamically generated from order records. + """ + def __init__(self, bill, order_record): self.bill = bill self.order = order_record.order @@ -114,6 +183,9 @@ class BillRecord(): raise Exception('Unsupported recurring period: {}.'. format(record.recurring_period)) +### +# Orders. + # /!\ BIG FAT WARNING /!\ # # # Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating @@ -190,63 +262,12 @@ class OrderRecord(models.Model): def ending_date(self): return self.order.ending_date -class PaymentMethod(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=False) - source = models.CharField(max_length=256, - choices = ( - ('stripe', 'Stripe'), - ('unknown', 'Unknown'), - ), - default='stripe') - description = models.TextField() - primary = models.BooleanField(default=True) - # Only used for "Stripe" source - stripe_card_id = models.CharField(max_length=32, blank=True, null=True) - - def charge(self, amount): - if amount > 0: # Make sure we don't charge negative amount by errors... - if self.source == 'stripe': - # TODO: wire to stripe, see meooow-payv1/strip_utils.py - payment = Payment(owner=self.owner, source=self.source, amount=amount) - payment.save() # TODO: Check return status - - return True - else: - # We do not handle that source yet. - return False - else: - return False - - class Meta: - unique_together = [['owner', 'primary']] - -class Payment(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - amount = models.DecimalField( - default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - source = models.CharField(max_length=256, - choices = ( - ('wire', 'Wire Transfer'), - ('stripe', 'Stripe'), - ('voucher', 'Voucher'), - ('referral', 'Referral'), - ('unknown', 'Unknown') - ), - default='unknown') - timestamp = models.DateTimeField(editable=False, auto_now_add=True) +### +# Products +# Abstract (= no database representation) class used as parent for products +# (e.g. uncloud_vm.models.VMProduct). class Product(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) owner = models.ForeignKey(get_user_model(), diff --git a/uncloud/uncloud_pay/serializers.py b/uncloud/uncloud_pay/serializers.py index d523b7a..6e4b2d3 100644 --- a/uncloud/uncloud_pay/serializers.py +++ b/uncloud/uncloud_pay/serializers.py @@ -9,36 +9,62 @@ from uncloud_vm.models import VMProduct import uncloud_pay.stripe as stripe -# TODO: remove magic numbers for decimal fields -class BillRecordSerializer(serializers.Serializer): - order = serializers.CharField() - description = serializers.CharField() - recurring_period = serializers.CharField() - recurring_price = serializers.DecimalField(max_digits=10, decimal_places=2) - amount = serializers.DecimalField(max_digits=10, decimal_places=2) +### +# Users. -class BillSerializer(serializers.ModelSerializer): - records = BillRecordSerializer(many=True, read_only=True) +class UserSerializer(serializers.ModelSerializer): class Meta: - model = Bill - fields = ['owner', 'total', 'due_date', 'creation_date', - 'starting_date', 'ending_date', 'records', 'final'] + model = get_user_model() + fields = ['username', 'email', 'balance'] + + # Display current 'balance' + balance = serializers.SerializerMethodField('get_balance') + def __sum_balance(self, entries): + return reduce(lambda acc, entry: acc + entry.amount, entries, 0) + + def get_balance(self, user): + return get_balance_for(user) + +### +# Payments and Payment Methods. class PaymentSerializer(serializers.ModelSerializer): class Meta: model = Payment fields = ['owner', 'amount', 'source', 'timestamp'] +class PaymentMethodSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentMethod + fields = ['source', 'description', 'primary'] + class CreditCardSerializer(serializers.Serializer): number = serializers.IntegerField() exp_month = serializers.IntegerField() exp_year = serializers.IntegerField() cvc = serializers.IntegerField() -class PaymentMethodSerializer(serializers.ModelSerializer): +class CreatePaymentMethodSerializer(serializers.ModelSerializer): + credit_card = CreditCardSerializer() + class Meta: model = PaymentMethod - fields = ['source', 'description', 'primary'] + fields = ['source', 'description', 'primary', 'credit_card'] + + def create(self, validated_data): + credit_card = stripe.CreditCard(**validated_data.pop('credit_card')) + user = self.context['request'].user + customer = stripe.create_customer(user.username, user.email) + # TODO check customer error + customer_id = customer['response_object']['id'] + stripe_card = stripe.create_card(customer_id, credit_card) + # TODO: check credit card error + validated_data['stripe_card_id'] = stripe_card['response_object']['id'] +class CreditCardSerializer(serializers.Serializer): + number = serializers.IntegerField() + exp_month = serializers.IntegerField() + exp_year = serializers.IntegerField() + cvc = serializers.IntegerField() class CreatePaymentMethodSerializer(serializers.ModelSerializer): credit_card = CreditCardSerializer() @@ -58,15 +84,36 @@ class CreatePaymentMethodSerializer(serializers.ModelSerializer): validated_data['stripe_card_id'] = stripe_card['response_object']['id'] payment_method = PaymentMethod.objects.create(**validated_data) return payment_method + payment_method = PaymentMethod.objects.create(**validated_data) + return payment_method -class ProductSerializer(serializers.Serializer): - vms = VMProductSerializer(many=True, read_only=True) +### +# Bills + +# TODO: remove magic numbers for decimal fields +class BillRecordSerializer(serializers.Serializer): + order = serializers.CharField() + description = serializers.CharField() + recurring_period = serializers.CharField() + recurring_price = serializers.DecimalField(max_digits=10, decimal_places=2) + amount = serializers.DecimalField(max_digits=10, decimal_places=2) + +class BillSerializer(serializers.ModelSerializer): + records = BillRecordSerializer(many=True, read_only=True) + class Meta: + model = Bill + fields = ['owner', 'total', 'due_date', 'creation_date', + 'starting_date', 'ending_date', 'records', 'final'] + +### +# Orders & Products. class OrderRecordSerializer(serializers.ModelSerializer): class Meta: model = OrderRecord fields = ['setup_fee', 'recurring_price', 'description'] + class OrderSerializer(serializers.ModelSerializer): records = OrderRecordSerializer(many=True, read_only=True) class Meta: @@ -74,15 +121,5 @@ class OrderSerializer(serializers.ModelSerializer): fields = ['uuid', 'creation_date', 'starting_date', 'ending_date', 'bill', 'recurring_period', 'records', 'recurring_price', 'setup_fee'] -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = get_user_model() - fields = ['username', 'email', 'balance'] - - # Display current 'balance' - balance = serializers.SerializerMethodField('get_balance') - def __sum_balance(self, entries): - return reduce(lambda acc, entry: acc + entry.amount, entries, 0) - - def get_balance(self, user): - return get_balance_for(user) +class ProductSerializer(serializers.Serializer): + vms = VMProductSerializer(many=True, read_only=True)