11from __future__ import annotations
22
3- import base64
4- import contextlib
53import datetime
64import functools
7- import hashlib
8- import hmac
95import typing
106
117from core .const .shop_error_messages import NotRefundableErrorMessages
128from core .models import BaseAbstractModel , BaseAbstractModelQuerySet
13- from core .util .strutil import uuid_to_b64
14- from django .conf import settings
9+ from core .scancode_mixin import ScanCodeMixin
1510from django .contrib .auth import get_user_model
1611from django .db import models
1712from django .db .models .manager import BaseManager
18- from rest_framework .reverse import reverse
19- from shop .payment_history .models import PaymentHistory
20- from shortuuid import decode , encode
13+ from shop .payment_history .models import PURCHASED_STATUSES , PaymentHistory
2114from simple_history .models import HistoricalRecords
2215
2316UserModel = get_user_model ()
@@ -30,8 +23,50 @@ def filter_has_payment_histories(self) -> models.QuerySet[Order]:
3023 def filter_has_no_payment_histories (self ) -> models .QuerySet [Order ]:
3124 return self .filter_active ().filter (~ models .Exists (PaymentHistory .objects .filter (order = models .OuterRef ("id" ))))
3225
26+ def filter_purchased_by (self , user : UserModel ) -> models .QuerySet [Order ]:
27+ """결제 완료/부분환불/환불된 (terminal status) 주문을 user 별로 필터."""
28+ return (
29+ self .filter_active ()
30+ .select_related ("customer_info" )
31+ .prefetch_related (
32+ models .Prefetch (
33+ lookup = "products" ,
34+ queryset = OrderProductRelation .objects .filter_active ()
35+ .select_related ("product" )
36+ .prefetch_related (
37+ models .Prefetch (
38+ lookup = "options" ,
39+ queryset = OrderProductOptionRelation .objects .filter_active ().select_related (
40+ "product_option_group" ,
41+ "product_option" ,
42+ ),
43+ ),
44+ ),
45+ ),
46+ models .Prefetch (
47+ "payment_histories" ,
48+ queryset = PaymentHistory .objects .filter_active ().order_by ("-created_at" ),
49+ to_attr = "_payment_histories_by_latest" ,
50+ ),
51+ )
52+ .annotate (
53+ current_status = (
54+ PaymentHistory .objects .filter (order_id = models .OuterRef ("id" ), status__in = PURCHASED_STATUSES )
55+ .order_by ("-created_at" )
56+ .values_list ("status" , flat = True )[:1 ]
57+ ),
58+ )
59+ .filter (user = user , current_status__in = PURCHASED_STATUSES )
60+ .order_by ("-created_at" )
61+ )
62+
63+ def filter_in_last_six_months (self ) -> models .QuerySet [Order ]:
64+ return self .filter (created_at__gte = datetime .date .today () - datetime .timedelta (days = 183 ))
65+
66+
67+ class Order (ScanCodeMixin , BaseAbstractModel ):
68+ scancode_prefix = "order"
3369
34- class Order (BaseAbstractModel ):
3570 user = models .ForeignKey (UserModel , on_delete = models .PROTECT )
3671 name = models .TextField ()
3772
@@ -42,7 +77,7 @@ class Order(BaseAbstractModel):
4277 prefetchs = {
4378 "_payment_histories_by_latest" : models .Prefetch (
4479 "payment_histories" ,
45- queryset = PaymentHistory .objects .order_by ("-created_at" ),
80+ queryset = PaymentHistory .objects .filter_active (). order_by ("-created_at" ),
4681 to_attr = "_payment_histories_by_latest" ,
4782 ),
4883 }
@@ -103,24 +138,6 @@ def is_cart(self) -> bool:
103138
104139 return self .current_status == PaymentHistoryStatus .pending
105140
106- @property
107- def short_id (self ) -> str :
108- """
109- 주문 ID를 base64로 인코딩한 문자열을 반환. QR 코드 생성에 사용됩니다.
110- (주문 ID의 길이를 줄여 QR 코드를 단순화하기 위함 - 길이가 길면 QR 코드가 복잡해짐)
111- """
112- return uuid_to_b64 (self .id )
113-
114- @property
115- def scancode_token (self ) -> str :
116- salted_order_id = f"{ self .id } { settings .SHOP .order_scancode_salt } " .encode ()
117- return hashlib .sha256 (salted_order_id ).hexdigest ()
118-
119- @property
120- def scancode_path (self ) -> str :
121- base_path = reverse ("v1:orders-retrieve-scancode" , kwargs = {"order_id" : self .id })
122- return f"{ base_path } ?token={ self .scancode_token } "
123-
124141 @property
125142 def not_fully_refundable_reason (self ) -> str | None :
126143 """
@@ -173,7 +190,9 @@ def not_fully_refundable_reason(self) -> str | None:
173190 return None
174191
175192
176- class OrderProductRelation (BaseAbstractModel ):
193+ class OrderProductRelation (ScanCodeMixin , BaseAbstractModel ):
194+ scancode_prefix = "opr"
195+
177196 class OrderProductStatus (models .TextChoices ):
178197 pending = "pending" , "결제 대기 중"
179198 paid = "paid" , "결제 완료"
@@ -197,48 +216,6 @@ class OrderProductStatus(models.TextChoices):
197216 def __str__ (self ) -> str :
198217 return f"[{ self .order } ] { self .product } ({ self .get_status_display ()} )"
199218
200- @functools .cached_property
201- def short_id (self ) -> str :
202- return encode (self .id )
203-
204- @functools .cached_property
205- def salt (self ) -> str :
206- hmac_result = hmac .new (settings .SHOP .order_scancode_salt .encode (), self .id .bytes , hashlib .sha256 ).digest ()
207- return base64 .urlsafe_b64encode (hmac_result ).decode ("utf-8" ).rstrip ("=" )
208-
209- @functools .cached_property
210- def scancode_token (self ) -> str :
211- return f"opr:{ self .short_id } :{ self .salt } "
212-
213- @functools .cached_property
214- def scancode_path (self ) -> str :
215- return f"{ reverse ('v1:order-products-scancode-list' )} ?token={ self .scancode_token } "
216-
217- @classmethod
218- def from_short_id (cls , short_id : str ) -> OrderProductRelation | None :
219- with contextlib .suppress (ValueError ):
220- return cls .objects .filter (id = decode (short_id )).first ()
221-
222- return None
223-
224- @classmethod
225- def from_scancode_token (cls , scancode_token : str ) -> OrderProductRelation | None :
226- splitted_token = scancode_token .split (":" )
227- if len (splitted_token ) != 3 :
228- return None
229-
230- prefix , short_id , salt = splitted_token
231- if prefix != "opr" :
232- return None
233-
234- if not (short_id and salt ):
235- return None
236-
237- if (opr := cls .from_short_id (short_id )) and opr .salt == salt :
238- return opr
239-
240- return None
241-
242219 @property
243220 def not_refundable_reason (self ) -> str | None :
244221 """
0 commit comments