Features: 1) Add support for shipping and billing addresses in serializers, mutations, and models; 2) Validate address inputs during order purchasing; 3) Auto-fill billing address if only shipping address is provided and vice-versa;

Fixes: 1) Correct redundant variable and file handling in geo management commands; 2) Fix formatting inconsistencies in tqdm loops across geo management commands; 3) Remove unnecessary decorator in token verification view;

Extra: Clean up imports, line breaks, and redundant code for better readability and maintainability in multiple files;
This commit is contained in:
Egor Pavlovich Gorbunov 2025-05-16 01:47:45 +03:00
parent 11edfb9d4f
commit 43c8df0c05
6 changed files with 111 additions and 83 deletions

View file

@ -172,26 +172,32 @@ class BuyOrder(BaseMutation):
force_balance = Boolean(required=False)
force_payment = Boolean(required=False)
promocode_uuid = UUID(required=False)
shipping_address = UUID(required=False)
billing_address = UUID(required=False)
order = Field(OrderType, required=False)
transaction = Field(TransactionType, required=False)
@staticmethod
def mutate(_parent, info, order_uuid=None, order_hr_id=None, force_balance=False, force_payment=False,
promocode_uuid=None):
promocode_uuid=None, shipping_address=None, billing_address=None):
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]):
raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive"))
user = info.context.user
try:
order = None
if order_uuid:
order = Order.objects.get(user=user, uuid=order_uuid)
if order_hr_id:
elif order_hr_id:
order = Order.objects.get(user=user, human_readable_id=order_hr_id)
instance = order.buy(
force_balance=force_balance, force_payment=force_payment, promocode_uuid=promocode_uuid
force_balance=force_balance, force_payment=force_payment, promocode_uuid=promocode_uuid,
shipping_address=shipping_address, billing_address=billing_address
)
match str(type(instance)):
case "<class 'payments.models.Transaction'>":
return BuyOrder(transaction=instance)
@ -199,6 +205,7 @@ class BuyOrder(BaseMutation):
return BuyOrder(order=instance)
case _:
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
except Order.DoesNotExist:
raise Http404(_(f"order {order_uuid} not found"))

View file

@ -625,7 +625,8 @@ class Order(NiceModel):
return promocode.use(self)
def buy(
self, force_balance: bool = False, force_payment: bool = False, promocode_uuid: str | None = None
self, force_balance: bool = False, force_payment: bool = False, promocode_uuid: str | None = None,
billing_address: str | None = None, shipping_address: str | None = None, **kwargs
) -> Self | Transaction | None:
if config.DISABLED_COMMERCE:
raise DisabledCommerceError(_("you can not buy at this moment, please try again in a few minutes"))
@ -633,6 +634,25 @@ class Order(NiceModel):
if (not force_balance and not force_payment) or (force_balance and force_payment):
raise ValueError(_("invalid force value"))
if not self.is_whole_digital and not any([shipping_address, billing_address]):
raise ValueError(_("you can only buy physical products with shipping address specified"))
if shipping_address and not billing_address:
shipping_address = Address.objects.get(uuid=shipping_address)
billing_address = shipping_address
elif billing_address and not shipping_address:
billing_address = Address.objects.get(uuid=billing_address)
shipping_address = billing_address
else:
billing_address = Address.objects.get(uuid=billing_address)
shipping_address = Address.objects.get(uuid=shipping_address)
self.billing_address = billing_address
self.shipping_address = shipping_address
self.save()
if self.total_quantity < 1:
raise ValueError(_("you cannot purchase an empty order!"))

View file

@ -70,6 +70,8 @@ class BuyOrderSerializer(Serializer):
force_balance = BooleanField(required=False, default=False)
force_payment = BooleanField(required=False, default=False)
promocode_uuid = CharField(required=False)
shipping_address_uuid = CharField(required=False)
billing_address_uuid = CharField(required=False)
class BuyUnregisteredOrderSerializer(Serializer):

View file

@ -243,6 +243,8 @@ class OrderViewSet(EvibesViewSet):
force_balance=serializer.validated_data.get("force_balance"),
force_payment=serializer.validated_data.get("force_payment"),
promocode_uuid=serializer.validated_data.get("promocode_uuid"),
shipping_address=serializer.validated_data.get("shipping_address"),
billing_address=serializer.validated_data.get("billing_address"),
)
match str(type(instance)):
case "<class 'payments.models.Transaction'>":

View file

@ -67,7 +67,6 @@ Continent = load_model("geo", "Continent")
Country = load_model("geo", "Country")
City = load_model("geo", "City")
# Only log errors during Travis tests
LOGGER_NAME = os.environ.get("TRAVIS_LOGGER_NAME", "geo")
@ -93,7 +92,7 @@ class Command(BaseCommand):
metavar="DATA_TYPES",
default="all",
help="Selectively import data. Comma separated list of data types: "
+ str(import_opts).replace("'", ""),
+ str(import_opts).replace("'", ""),
),
make_option(
"--flush",
@ -198,9 +197,8 @@ class Command(BaseCommand):
if not os.path.exists(self.data_dir):
os.makedirs(self.data_dir)
with open(os.path.join(self.data_dir, filename), "wb") as f:
file = f
file.write(web_file.read())
file.close()
f.write(web_file.read())
f.close()
elif not os.path.exists(os.path.join(self.data_dir, filename)):
raise Exception(f"File not found and download failed: {filename} [{url}]")
@ -258,10 +256,10 @@ class Command(BaseCommand):
import_continents_as_fks = isinstance(Country._meta.get_field("continent"), ForeignKey)
for item in tqdm(
[d for d in data if d["code"] not in NO_LONGER_EXISTENT_COUNTRY_CODES],
disable=self.options.get("quiet"),
total=total,
desc="Importing countries",
[d for d in data if d["code"] not in NO_LONGER_EXISTENT_COUNTRY_CODES],
disable=self.options.get("quiet"),
total=total,
desc="Importing countries",
):
if not self.call_hook("country_pre", item):
continue
@ -314,10 +312,10 @@ class Command(BaseCommand):
continue
for country, neighbour_codes in tqdm(
list(neighbours.items()),
disable=self.options.get("quiet"),
total=len(neighbours),
desc="Importing country neighbours",
list(neighbours.items()),
disable=self.options.get("quiet"),
total=len(neighbours),
desc="Importing country neighbours",
):
neighbours = [x for x in [countries.get(x) for x in neighbour_codes if x] if x]
country.neighbours.add(*neighbours)
@ -328,10 +326,10 @@ class Command(BaseCommand):
self.country_index = {}
for obj in tqdm(
Country.objects.all(),
disable=self.options.get("quiet"),
total=Country.objects.all().count(),
desc="Building country index",
Country.objects.all(),
disable=self.options.get("quiet"),
total=Country.objects.all().count(),
desc="Building country index",
):
self.country_index[obj.code] = obj
@ -347,10 +345,10 @@ class Command(BaseCommand):
countries_not_found = {}
for item in tqdm(
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing regions",
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing regions",
):
if not self.call_hook("region_pre", item):
continue
@ -409,13 +407,13 @@ class Command(BaseCommand):
self.region_index = {}
for obj in tqdm(
chain(
Region.objects.all().prefetch_related("country"),
Subregion.objects.all().prefetch_related("region__country"),
),
disable=self.options.get("quiet"),
total=Region.objects.all().count() + Subregion.objects.all().count(),
desc="Building region index",
chain(
Region.objects.all().prefetch_related("country"),
Subregion.objects.all().prefetch_related("region__country"),
),
disable=self.options.get("quiet"),
total=Region.objects.all().count() + Subregion.objects.all().count(),
desc="Building region index",
):
self.region_index[obj.full_code()] = obj
@ -432,10 +430,10 @@ class Command(BaseCommand):
regions_not_found = {}
for item in tqdm(
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing subregions",
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing subregions",
):
if not self.call_hook("subregion_pre", item):
continue
@ -503,10 +501,10 @@ class Command(BaseCommand):
self.build_region_index()
for item in tqdm(
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing cities",
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing cities",
):
if not self.call_hook("city_pre", item):
continue
@ -616,10 +614,10 @@ class Command(BaseCommand):
self.hierarchy = {}
for item in tqdm(
data,
disable=self.options.get("quiet"),
total=total,
desc="Building hierarchy index",
data,
disable=self.options.get("quiet"),
total=total,
desc="Building hierarchy index",
):
parent_id = int(item["parent"])
child_id = int(item["child"])
@ -639,18 +637,18 @@ class Command(BaseCommand):
city_index = {}
for obj in tqdm(
City.objects.all(),
disable=self.options.get("quiet"),
total=City.objects.all().count(),
desc="Building city index",
City.objects.all(),
disable=self.options.get("quiet"),
total=City.objects.all().count(),
desc="Building city index",
):
city_index[obj.id] = obj
for item in tqdm(
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing districts",
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing districts",
):
if not self.call_hook("district_pre", item):
continue
@ -766,10 +764,10 @@ class Command(BaseCommand):
for type_ in (Country, Region, Subregion, City, District):
plural_type_name = f"{type_.__name__}s" if type_.__name__[-1] != "y" else f"{type_.__name__[:-1]}ies"
for obj in tqdm(
type_.objects.all(),
disable=self.options.get("quiet"),
total=type_.objects.all().count(),
desc=f"Building geo index for {plural_type_name.lower()}",
type_.objects.all(),
disable=self.options.get("quiet"),
total=type_.objects.all().count(),
desc=f"Building geo index for {plural_type_name.lower()}",
):
geo_index[obj.id] = {
"type": type_,
@ -777,10 +775,10 @@ class Command(BaseCommand):
}
for item in tqdm(
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing data for alternative names",
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing data for alternative names",
):
if not self.call_hook("alt_name_pre", item):
continue
@ -897,10 +895,10 @@ class Command(BaseCommand):
self.postal_code_regex_index = {}
for code, country in tqdm(
self.country_index.items(),
disable=self.options.get("quiet"),
total=len(self.country_index),
desc="Building postal code regex index",
self.country_index.items(),
disable=self.options.get("quiet"),
total=len(self.country_index),
desc="Building postal code regex index",
):
try:
self.postal_code_regex_index[code] = re.compile(country.postal_code_regex)
@ -928,10 +926,10 @@ class Command(BaseCommand):
if num_existing_postal_codes == 0:
self.logger.debug("Zero postal codes found - using only-create postal code optimization")
for item in tqdm(
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing postal codes",
data,
disable=self.options.get("quiet"),
total=total,
desc="Importing postal codes",
):
if not self.call_hook("postal_code_pre", item):
continue
@ -1137,16 +1135,16 @@ class Command(BaseCommand):
)
# If they're both part of the same city
if (
District.objects.filter(
Q(city__region__name_std__iexact=pc.region_name)
| Q(city__region__name__iexact=pc.region_name),
Q(name_std__iexact=pc.district_name) | Q(name__iexact=pc.district_name),
city__country=pc.country,
)
.values_list("city")
.distinct()
.count()
== 1
District.objects.filter(
Q(city__region__name_std__iexact=pc.region_name)
| Q(city__region__name__iexact=pc.region_name),
Q(name_std__iexact=pc.district_name) | Q(name__iexact=pc.district_name),
city__country=pc.country,
)
.values_list("city")
.distinct()
.count()
== 1
):
# Use the one with the lower ID
pc.district = (
@ -1244,9 +1242,9 @@ class Command(BaseCommand):
for type_ in (Country, Region, Subregion, City, District, PostalCode):
plural_type_name = type_.__name__ if type_.__name__[-1] != "y" else f"{type_.__name__[:-1]}ies"
for obj in tqdm(
type_.objects.all(),
disable=self.options.get("quiet"),
total=type_.objects.count(),
desc=f"Flushing alternative names for {plural_type_name}",
type_.objects.all(),
disable=self.options.get("quiet"),
total=type_.objects.count(),
desc=f"Flushing alternative names for {plural_type_name}",
):
obj.alt_names.all().delete()

View file

@ -47,7 +47,6 @@ class TokenVerifyView(TokenViewBase):
serializer_class = TokenVerifySerializer
_serializer_class = TokenVerifySerializer
@method_decorator(ratelimit(key="ip", rate="10/h" if not DEBUG else "888/h"))
def post(self, request, *args, **kwargs):
try:
serializer = self.get_serializer(data=request.data)