# BaseApp Payments - Django
This app provides the integration of Stripe with The SilverLogic's [BaseApp](https://bitbucket.org/silverlogic/baseapp-django-v2): [django-restframework](https://www.django-rest-framework.org/) and [dj-stripe](https://dj-stripe.readthedocs.io/en/master/)
## Install the package
Add to `requirements/base.txt`:
```bash
baseapp-payments==0.16.1
```
## Setup Stripe's credentials
Add to your `settings/base.py`:
```py
# Stripe
STRIPE_LIVE_SECRET_KEY = env("STRIPE_LIVE_SECRET_KEY")
STRIPE_TEST_SECRET_KEY = env("STRIPE_TEST_SECRET_KEY")
STRIPE_LIVE_MODE = env("STRIPE_LIVE_MODE") # Change to True in production
DJSTRIPE_WEBHOOK_SECRET = env("DJSTRIPE_WEBHOOK_SECRET")
DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id"
```
## Add the payments_router to your urlpatterns
```py
from baseapp_payments.router import payments_router
v1_urlpatterns = [
...
re_path(r"payments", include(payments_router.urls)),
...
]
```
## Subscriber
A subscriber can be an User, an Organization, a Project, any model that have an `email` property. You can specify the model of your subscriber with the setting:
```py
DJSTRIPE_SUBSCRIBER_MODEL='apps.organizations.Organization`
```
Make sure to also implement `get_subscriber_from_request` in your `apps.users.User` to grab the subscriber for the current authenticated user:
```py
class User(PermissionsMixin, AbstractBaseUser):
...
def get_subscriber_from_request(self, request):
org_pk = request.GET.get('organization')
return Organization.objects.get(pk=org_pk, admins=request.user)
```
Implement the following methods in the subscriber's model:
```py
class Organization(models.Model):
def get_subscription_plan(self):
return self.subscription_plan
def subscription_start_request(self, plan, customer, subscription, request):
self.subscription_plan = plan
self.show_payment_method_action_banner = False
self.save()
def subscription_cancel_request(self, customer, subscription, request):
# in this use case the self.subscription_plan will be set to null when we receive the event from stripe instead
pass
def subscription_update_request(self, plan, is_upgrade, request):
# is_upgrade = current plan's price < new plan's price
# if we want to upgrade right way but wait to the end of the period to change plans when it is a downgrade:
if is_upgrade:
self.subscription_plan = plan
self.save()
def subscription_plan_changed_webhook(self, plan, price, event):
# stripe's event: invoice.paid
# this method is called if the plan is different from the one returned by self.get_subscription_plan()
self.subscription_plan = plan
self.save()
def subscription_deleted_webhook(self, event):
# stripe's event: customer.subscription.deleted
self.subscription_plan = None
self.show_payment_method_action_banner = True
self.save()
def invoice_payment_failed_webhook(self, event):
# stripe's event: invoice.payment_failed
self.show_payment_method_action_banner = True
self.save()
```
## Plan model
You can extend the plan model by inheriting `baseapp_payments.models.BasePlan`:
```py
from django.db import models
from baseapp_payments.models import BasePlan
class SubscriptionPlan(BasePlan):
video_calls_per_month = models.PositiveIntegerField(default=5)
```
Add to your `settings/base.py` the path to your custom plan model:
```
BASEAPP_PAYMENTS_PLAN_MODEL = "apps.plans.SubscriptionPlan"
```
To **extend the serializer** you can create a normal serializer:
```py
from baseapp_payments.serializers import PlanSerializer
from .models import SubscriptionPlan
class SubscriptionPlanSerializer(PlanSerializer):
class Meta:
model = SubscriptionPlan
fields = super().Meta.fields + (
"searches_per_month",
"can_create_favorites",
)
```
Then add to your `settings/base.py` the path to your custom serializer:
```py
BASEAPP_PAYMENTS_PLAN_SERIALIZER = "apps.plans.serializers.SubscriptionPlanSerializer"
```
## One time payment / buy a product
Implement method stripe_payment_intent_params in your product model:
```py
def stripe_payment_intent_params(self, request, validated_data):
price = self.price
if self.class_type.slug == "donation":
price = validated_data["amount"]
amount = int(price * 100)
return {
"amount": amount,
"application_fee_amount": int(self.instructor.stripe_percentage_fee / 100.00 * amount),
"transfer_data": {"destination": self.instructor.stripe_account_id,},
}
```
And then call create_payment_intent in the Viewset you're using for 'checkout/purchase' your product:
[FLOW Example](https://bitbucket.org/silverlogic/flow-backend-django/src/64dbb17acfd05333fb6177c6b2e42c2332d89571/apps/api/v1/classes/views.py?at=master#views.py-178)
```py
from baseapp_payments.utils import create_payment_intent, stripe
@action(
detail=True,
methods=["POST"],
permission_classes=[permissions.IsAuthenticated],
serializer_class=PurchaseClassSerializer,
)
def purchase(self, request, *args, **kwargs):
class_obj = self.get_object()
user = request.user
serializer = self.get_serializer(data=request.data)
serializer.class_obj = class_obj
serializer.is_valid(raise_exception=True)
if class_obj.class_type.slug != "free":
payment_intent = create_payment_intent(class_obj, request, serializer.validated_data)
ClassStudent.objects.create(student=user, clss=class_obj, payment_intent=payment_intent)
else:
ClassStudent.objects.create(student=user, clss=class_obj)
serializer.send_email()
return response.Response({}, status=status.HTTP_200_OK)
# Stripe's webhook events
You can [listen to any stripe events using the webhooks](https://dj-stripe.readthedocs.io/en/master/usage/webhooks/)
```py
from djstripe import webhooks
@webhooks.handler("customer.subscription.trial_will_end")
def my_handler(event, **kwargs):
event.customer.subscriber.show_trial_ended_action_banner = True
event.customer.subscriber.save()
```
# Two-way sync with Stripe
When the webhook is fully setup all data changed in stripe will be updated in the system receiving the webhooks events. If you have data on Stripe already you can [manually sync](https://dj-stripe.readthedocs.io/en/master/usage/manually_syncing_with_stripe/). For example with the following command you can sync all data:
```bash
./manage.py djstripe_sync_models
```
# To do
- [ ] Create a special error message to be handled by the frontend package
Ex: if by trying to perform an action I'm not able due to payment failure or plan is out of credits for that action then show call to action to upgrade
- [ ] Move tests from FinJoy to this repository
- [ ] One time payments (to buy a product)
Raw data
{
"_id": null,
"home_page": "https://bitbucket.org/silverlogic/baseapp-payments-django",
"name": "baseapp-payments",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": null,
"author": "The SilverLogic",
"author_email": "dev@tsl.io",
"download_url": "https://files.pythonhosted.org/packages/2d/31/7ad2ef24ee7028d0db90610a94491ccbd78333598a919cb4ac1592d65954/baseapp-payments-0.16.2.tar.gz",
"platform": null,
"description": "# BaseApp Payments - Django\n\nThis app provides the integration of Stripe with The SilverLogic's [BaseApp](https://bitbucket.org/silverlogic/baseapp-django-v2): [django-restframework](https://www.django-rest-framework.org/) and [dj-stripe](https://dj-stripe.readthedocs.io/en/master/)\n\n## Install the package\n\nAdd to `requirements/base.txt`:\n\n```bash\nbaseapp-payments==0.16.1\n```\n\n## Setup Stripe's credentials\n\nAdd to your `settings/base.py`:\n\n```py\n# Stripe\nSTRIPE_LIVE_SECRET_KEY = env(\"STRIPE_LIVE_SECRET_KEY\")\nSTRIPE_TEST_SECRET_KEY = env(\"STRIPE_TEST_SECRET_KEY\")\nSTRIPE_LIVE_MODE = env(\"STRIPE_LIVE_MODE\") # Change to True in production\nDJSTRIPE_WEBHOOK_SECRET = env(\"DJSTRIPE_WEBHOOK_SECRET\")\nDJSTRIPE_FOREIGN_KEY_TO_FIELD = \"id\"\n```\n\n## Add the payments_router to your urlpatterns\n\n```py\nfrom baseapp_payments.router import payments_router\n\nv1_urlpatterns = [\n ...\n re_path(r\"payments\", include(payments_router.urls)),\n ...\n]\n```\n\n## Subscriber\n\nA subscriber can be an User, an Organization, a Project, any model that have an `email` property. You can specify the model of your subscriber with the setting:\n\n```py\nDJSTRIPE_SUBSCRIBER_MODEL='apps.organizations.Organization`\n```\n\nMake sure to also implement `get_subscriber_from_request` in your `apps.users.User` to grab the subscriber for the current authenticated user:\n\n```py\nclass User(PermissionsMixin, AbstractBaseUser):\n ...\n\n def get_subscriber_from_request(self, request):\n org_pk = request.GET.get('organization')\n return Organization.objects.get(pk=org_pk, admins=request.user)\n```\n\nImplement the following methods in the subscriber's model:\n\n```py\nclass Organization(models.Model):\n def get_subscription_plan(self):\n return self.subscription_plan\n\n def subscription_start_request(self, plan, customer, subscription, request):\n self.subscription_plan = plan\n self.show_payment_method_action_banner = False\n self.save()\n\n def subscription_cancel_request(self, customer, subscription, request):\n # in this use case the self.subscription_plan will be set to null when we receive the event from stripe instead\n pass\n\n def subscription_update_request(self, plan, is_upgrade, request):\n # is_upgrade = current plan's price < new plan's price\n\n # if we want to upgrade right way but wait to the end of the period to change plans when it is a downgrade:\n if is_upgrade:\n self.subscription_plan = plan\n self.save()\n\n def subscription_plan_changed_webhook(self, plan, price, event):\n # stripe's event: invoice.paid\n # this method is called if the plan is different from the one returned by self.get_subscription_plan()\n self.subscription_plan = plan\n self.save()\n\n def subscription_deleted_webhook(self, event):\n # stripe's event: customer.subscription.deleted\n self.subscription_plan = None\n self.show_payment_method_action_banner = True\n self.save()\n\n def invoice_payment_failed_webhook(self, event):\n # stripe's event: invoice.payment_failed\n self.show_payment_method_action_banner = True\n self.save()\n```\n\n## Plan model\n\nYou can extend the plan model by inheriting `baseapp_payments.models.BasePlan`:\n\n```py\nfrom django.db import models\nfrom baseapp_payments.models import BasePlan\n\nclass SubscriptionPlan(BasePlan):\n video_calls_per_month = models.PositiveIntegerField(default=5)\n```\n\nAdd to your `settings/base.py` the path to your custom plan model:\n\n```\nBASEAPP_PAYMENTS_PLAN_MODEL = \"apps.plans.SubscriptionPlan\"\n```\n\nTo **extend the serializer** you can create a normal serializer:\n\n```py\n\nfrom baseapp_payments.serializers import PlanSerializer\nfrom .models import SubscriptionPlan\n\nclass SubscriptionPlanSerializer(PlanSerializer):\n class Meta:\n model = SubscriptionPlan\n fields = super().Meta.fields + (\n \"searches_per_month\",\n \"can_create_favorites\",\n )\n\n```\n\nThen add to your `settings/base.py` the path to your custom serializer:\n\n```py\nBASEAPP_PAYMENTS_PLAN_SERIALIZER = \"apps.plans.serializers.SubscriptionPlanSerializer\"\n```\n\n## One time payment / buy a product\n\n\nImplement method stripe_payment_intent_params in your product model:\n\n```py\n def stripe_payment_intent_params(self, request, validated_data):\n price = self.price\n\n if self.class_type.slug == \"donation\":\n price = validated_data[\"amount\"]\n\n amount = int(price * 100)\n\n return {\n\n \"amount\": amount,\n \"application_fee_amount\": int(self.instructor.stripe_percentage_fee / 100.00 * amount),\n \"transfer_data\": {\"destination\": self.instructor.stripe_account_id,},\n\n }\n```\n\nAnd then call create_payment_intent in the Viewset you're using for 'checkout/purchase' your product:\n [FLOW Example](https://bitbucket.org/silverlogic/flow-backend-django/src/64dbb17acfd05333fb6177c6b2e42c2332d89571/apps/api/v1/classes/views.py?at=master#views.py-178)\n```py\n from baseapp_payments.utils import create_payment_intent, stripe\n\n\n @action(\n detail=True,\n methods=[\"POST\"],\n permission_classes=[permissions.IsAuthenticated],\n serializer_class=PurchaseClassSerializer,\n )\n def purchase(self, request, *args, **kwargs):\n class_obj = self.get_object()\n user = request.user\n serializer = self.get_serializer(data=request.data)\n serializer.class_obj = class_obj\n serializer.is_valid(raise_exception=True)\n\n if class_obj.class_type.slug != \"free\":\n payment_intent = create_payment_intent(class_obj, request, serializer.validated_data)\n ClassStudent.objects.create(student=user, clss=class_obj, payment_intent=payment_intent)\n else:\n ClassStudent.objects.create(student=user, clss=class_obj)\n\n serializer.send_email()\n return response.Response({}, status=status.HTTP_200_OK)\n \n# Stripe's webhook events\n\nYou can [listen to any stripe events using the webhooks](https://dj-stripe.readthedocs.io/en/master/usage/webhooks/) \n\n```py\nfrom djstripe import webhooks\n\n@webhooks.handler(\"customer.subscription.trial_will_end\")\ndef my_handler(event, **kwargs):\n event.customer.subscriber.show_trial_ended_action_banner = True\n event.customer.subscriber.save()\n```\n\n# Two-way sync with Stripe\n\nWhen the webhook is fully setup all data changed in stripe will be updated in the system receiving the webhooks events. If you have data on Stripe already you can [manually sync](https://dj-stripe.readthedocs.io/en/master/usage/manually_syncing_with_stripe/). For example with the following command you can sync all data:\n\n```bash\n./manage.py djstripe_sync_models\n```\n\n\n# To do\n\n - [ ] Create a special error message to be handled by the frontend package\n Ex: if by trying to perform an action I'm not able due to payment failure or plan is out of credits for that action then show call to action to upgrade\n - [ ] Move tests from FinJoy to this repository\n - [ ] One time payments (to buy a product)",
"bugtrack_url": null,
"license": "BSD-3-Clause # Example license",
"summary": "A BaseApp app to handle payments.",
"version": "0.16.2",
"project_urls": {
"Homepage": "https://bitbucket.org/silverlogic/baseapp-payments-django"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "2d317ad2ef24ee7028d0db90610a94491ccbd78333598a919cb4ac1592d65954",
"md5": "a648f027fe82a51c865842e21e692b0c",
"sha256": "2c29af8543846d03a573c34629813ff4dc8732fe128fd6d140012e2181e9aaa2"
},
"downloads": -1,
"filename": "baseapp-payments-0.16.2.tar.gz",
"has_sig": false,
"md5_digest": "a648f027fe82a51c865842e21e692b0c",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 14588,
"upload_time": "2024-09-03T12:13:29",
"upload_time_iso_8601": "2024-09-03T12:13:29.185744Z",
"url": "https://files.pythonhosted.org/packages/2d/31/7ad2ef24ee7028d0db90610a94491ccbd78333598a919cb4ac1592d65954/baseapp-payments-0.16.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-09-03 12:13:29",
"github": false,
"gitlab": false,
"bitbucket": true,
"codeberg": false,
"bitbucket_user": "silverlogic",
"bitbucket_project": "baseapp-payments-django",
"lcname": "baseapp-payments"
}