Skip to content

Commit

Permalink
[maykinmedia/open-producten#24] Add product price logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Floris272 committed Oct 29, 2024
1 parent f28ea9d commit af7bc7e
Show file tree
Hide file tree
Showing 25 changed files with 700 additions and 9 deletions.
4 changes: 4 additions & 0 deletions src/openforms/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from openforms.utils.urls import decorator_include
from openforms.variables.api.viewsets import ServiceFetchConfigurationViewSet

from ..contrib.open_producten.api.viewsets import PriceViewSet
from .views import PingView

# from .schema import schema_view
Expand Down Expand Up @@ -58,6 +59,9 @@
# products
router.register("products", ProductViewSet)

# product prices (Open Producten)
router.register("product_prices", PriceViewSet, "prices")

# services
router.register("services", ServiceViewSet)

Expand Down
1 change: 1 addition & 0 deletions src/openforms/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
"openforms.contrib.haal_centraal",
"openforms.contrib.kadaster",
"openforms.contrib.kvk",
"openforms.contrib.open_producten",
"openforms.contrib.microsoft.apps.MicrosoftApp",
"openforms.contrib.objects_api",
"openforms.dmn",
Expand Down
1 change: 1 addition & 0 deletions src/openforms/contrib/open_producten/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PRICE_OPTION_KEY = "productPrice"
21 changes: 21 additions & 0 deletions src/openforms/contrib/open_producten/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from solo.admin import SingletonModelAdmin

from .models import OpenProductenConfig


@admin.register(OpenProductenConfig)
class OpenProductenConfigAdmin(SingletonModelAdmin):

fieldsets = [
(
_("Services"),
{
"fields": [
"producten_service",
],
},
)
]
Empty file.
27 changes: 27 additions & 0 deletions src/openforms/contrib/open_producten/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from rest_framework import serializers

from openforms.products.models import Product as ProductType

from ..models import Price, PriceOption


class PriceOptionSerializer(serializers.ModelSerializer):
class Meta:
model = PriceOption
exclude = ("price",)


class PriceSerializer(serializers.ModelSerializer):
options = PriceOptionSerializer(many=True)

class Meta:
model = Price
fields = "__all__"


class ProductTypeSerializer(serializers.ModelSerializer):
open_producten_price = PriceSerializer()

class Meta:
model = ProductType
exclude = ("id", "information", "price")
41 changes: 41 additions & 0 deletions src/openforms/contrib/open_producten/api/viewsets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.utils.translation import gettext_lazy as _

from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import authentication, permissions, viewsets

from openforms.products.models import Product as ProductType

from .serializers import ProductTypeSerializer


@extend_schema_view(
list=extend_schema(
summary=_("List available products with current price options"),
),
retrieve=extend_schema(
summary=_("Retrieve details of a single product"),
),
)
class PriceViewSet(viewsets.ReadOnlyModelViewSet):
"""
List and retrieve the products registered in the admin interface.
Note that these endpoints are only available to authenticated admin users. The
products functionality is minimal to be able to register prices. In the future,
probably a dedicated products catalogue will become relevant.
"""

# queryset = ProductType.objects.all()
authentication_classes = (
authentication.SessionAuthentication,
authentication.TokenAuthentication,
)
permission_classes = (permissions.IsAdminUser,)
serializer_class = ProductTypeSerializer
lookup_field = "open_product_uuid"
lookup_url_kwarg = "uuid"

def get_queryset(self):
return ProductType.objects.filter(
open_producten_price__isnull=False, is_deleted=False
).distinct()
28 changes: 28 additions & 0 deletions src/openforms/contrib/open_producten/api_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from dataclasses import dataclass
from datetime import date
from typing import Optional

from zgw_consumers.api_models.base import Model


@dataclass
class PriceOption(Model):
id: str
amount: str
description: str


@dataclass
class Price(Model):
id: str
valid_from: date
options: list[PriceOption]


@dataclass
class ProductType(Model):
id: str
name: str
current_price: Optional[Price]
upl_name: str
upl_uri: str
8 changes: 8 additions & 0 deletions src/openforms/contrib/open_producten/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class OpenProductenConfig(AppConfig):
name = "openforms.contrib.open_producten"
label = "open_producten"
verbose_name = _("Open Producten configuration")
40 changes: 40 additions & 0 deletions src/openforms/contrib/open_producten/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging

import requests
from ape_pie import APIClient
from zgw_consumers.api_models.base import factory
from zgw_consumers.client import build_client

from openforms.contrib.open_producten.models import OpenProductenConfig

from .api_models import ProductType

logger = logging.getLogger(__name__)


class OpenProductenClient(APIClient):
def get_current_prices(self) -> list[ProductType]:
try:
response = self.get("producttypes/current-prices")
response.raise_for_status()
except requests.RequestException as exc:
logger.exception(
"exception while making KVK basisprofiel request", exc_info=exc
)
raise exc

data = response.json()
product_types = factory(ProductType, data)

return product_types


class NoServiceConfigured(RuntimeError):
pass


def get_open_producten_client() -> OpenProductenClient:
config = OpenProductenConfig.get_solo()
if not (service := config.producten_service):
raise NoServiceConfigured("No open producten service configured!")
return build_client(service, client_factory=OpenProductenClient)
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.core.management.base import BaseCommand

from openforms.contrib.open_producten.client import get_open_producten_client
from openforms.contrib.open_producten.models import OpenProductenConfig
from openforms.contrib.open_producten.price_import import PriceImporter


class Command(BaseCommand):
help = "Import product types"

def handle(self, *args, **options):
if OpenProductenConfig.objects.count() == 0:
self.stdout.write(
"Please define the OpenProductenConfig before running this command."
)
return

client = get_open_producten_client()
price_importer = PriceImporter(client)

(
created,
updated,
deleted_count,
soft_deleted_count,
) = price_importer.import_product_types()

self.stdout.write(f"deleted {deleted_count} product type(s):\n")
self.stdout.write(f"soft deleted {soft_deleted_count} product type(s):\n")
self.stdout.write(f"updated {len(updated)} exising product type(s)")
self.stdout.write(f"created {len(created)} new product type(s):\n")

for instance in created:
self.stdout.write(f"{type(instance).__name__}: {instance.uuid}")
110 changes: 110 additions & 0 deletions src/openforms/contrib/open_producten/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Generated by Django 4.2.16 on 2024-10-10 12:59

import datetime
from decimal import Decimal
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
("zgw_consumers", "0020_service_timeout"),
]

operations = [
migrations.CreateModel(
name="Price",
fields=[
("uuid", models.UUIDField(primary_key=True, serialize=False)),
(
"valid_from",
models.DateField(
help_text="The date at which this price is valid",
unique=True,
validators=[
django.core.validators.MinValueValidator(
datetime.date.today
)
],
verbose_name="Start date",
),
),
],
options={
"verbose_name": "Price",
"verbose_name_plural": "Prices",
},
),
migrations.CreateModel(
name="PriceOption",
fields=[
("uuid", models.UUIDField(primary_key=True, serialize=False)),
(
"amount",
models.DecimalField(
decimal_places=2,
help_text="The amount of the price option",
max_digits=8,
validators=[
django.core.validators.MinValueValidator(Decimal("0.01"))
],
verbose_name="Price",
),
),
(
"description",
models.CharField(
help_text="Short description of the option",
max_length=100,
verbose_name="Description",
),
),
(
"price",
models.ForeignKey(
help_text="The price this option belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="options",
to="open_producten.price",
verbose_name="Price",
),
),
],
options={
"verbose_name": "Price option",
"verbose_name_plural": "Price options",
},
),
migrations.CreateModel(
name="OpenProductenConfig",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"producten_service",
models.OneToOneField(
limit_choices_to={"api_type": "orc"},
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="zgw_consumers.service",
verbose_name="Producten API",
),
),
],
options={
"verbose_name": "Open Producten configuration",
},
),
]
Empty file.
Loading

0 comments on commit af7bc7e

Please sign in to comment.