feat: Payment repo를 backend repo에 병합#43
Conversation
| queryset = ( | ||
| Order.objects.filter_has_payment_histories() | ||
| .filter(models.Exists(OrderProductRelation.objects.filter(order=models.OuterRef("pk")))) |
There was a problem hiding this comment.
오 django queryset에 OuterRef라는 기능도 있었군요 신기하네요...
There was a problem hiding this comment.
요것도 우선순위 낮은 코멘트이긴 한데요..! queryset이 길어서 하위 QuerySet (SubQuerySet..? 명칭을 뭐라고 하는지 정확히 모르겠네요ㅠㅠ) 같은 걸 사용하면 가독성이 조금 더 좋아질 것 같습니다!
| @staticmethod | ||
| def _lock_or_promote_order(obj_id: str) -> Order: | ||
| """Order 가 있으면 lock 하여 반환. SingleProductCart 만 있으면 lock + to_order() 로 승격. | ||
|
|
||
| 동시 webhook race 시: 첫 호출이 cart lock + to_order() commit 후, 두 번째 호출은 | ||
| cart 가 hard_delete 된 상태로 lock 해제됨 → Order 재조회에서 승격된 Order 발견. | ||
| """ | ||
| if order := Order.objects.select_for_update().filter_active().filter(id=obj_id).first(): | ||
| return order | ||
| if cart := SingleProductCart.objects.select_for_update().filter_active().filter(id=obj_id).first(): | ||
| return cart.to_order() | ||
| # 첫 lock 시 cart 가 다른 webhook 에 의해 promote 된 경우 — Order 재조회. | ||
| if order := Order.objects.select_for_update().filter_active().filter(id=obj_id).first(): | ||
| return order | ||
| raise serializers.ValidationError(detail=PortOneWebhookFailureMessages.ORDER_NOT_FOUND, code="forgery") |
There was a problem hiding this comment.
오 여기서 Order이나 SingleProductCart를 반환하는데, 혹시 SingleProductCart나 Order 모델이 어떤 차이가 있는지 간단하게 코멘트 달아주심 나중에 다른 분들이 보시기에 조금 덜 헷갈릴 것 같습니다!
There was a problem hiding this comment.
542b44e에서 추가 설명 추가했슴다, 다만 요건 나중에 구조 문서를 별도로 만드는 쪽이 확실하겠네유
| @@ -0,0 +1,606 @@ | |||
| import enum | |||
There was a problem hiding this comment.
요거는 아주 사소한 사항이긴 한데, 파일이 길어서 app/shop/serializers/cart/ 디렉토리 안에다가 Tag, Option, Product 등등으로 나눠도 좋을 것 같아요!
| if not any(o.current_status != PaymentHistoryStatus.refunded for o in orders): | ||
| raise _ScanCodeError(msg="최근 6개월 이내에 결제한 주문이 없습니다.", code=status.HTTP_403_FORBIDDEN) |
There was a problem hiding this comment.
환불한 사람도 여기에 해당될 수 있을 것 같아서, 메시지를 "최근 6개월 이내에 결제된 유효한 주문이 없습니다(환불 완료 또는 주문 없음)" 로 추가해도 좋을 것 같습니다!
| def _get_or_create_api_key_user(api_key: str) -> "UserExt": | ||
| from user.models import UserExt | ||
|
|
||
| username = f"API_KEY_USER_{api_key.upper()}" | ||
| email = f"api_key_user_{api_key.lower()}@pycon.kr" |
There was a problem hiding this comment.
(가능성이 적어서 우선순위는 낮아 보이긴 합니다..!) 혹시 api key가 중간에 바뀌게 된다면 이전 api key로 만들어진 유저들을 식별하기 어려울 수도 있을 것 같아요.
어떤 방법이 좋을지는 모르겠는데... APIKey 모델을 만드는 것도 하나의 대안 같습니다!
There was a problem hiding this comment.
API Key는 저희 서비스 (ex: 등록 데스크)를 호출하는 타 서비스가 로그인 인증 대신 사용하도록 추가한 것이긴 하거든요, 즉 서비스별로 API Key가 발급되는 것이 의도한 바이긴 합니다! 그리고 key rotation이 발생하면 해당 타 서비스가 접근 못하게 되는 것이 오히려 의도한 바입니다, API Key가 유출되면 접근을 막는 것이 맞아 보여서요...! 다만 key rotation 후 기존 key로는 유저를 완전 추측하지 못하게 되는 것은 좀 아쉬운 점이긴 하네요, 이 부분에서는 확실히 테이블을 추가하고, simple history를 묶는 것이 낫겠습니다 🤔
다만 요 PR 내용이 그렇잖아도 무지막지하게 긴 지라(...) 그리고 API Key가 당장 사용되지는 않으므로 (사실 payment에 기능을 그대로 옮겨온 것에 좀 더 가깝긴 합니다 😅 다만 개인적으로 개발 중인 등록 데스크 쪽에서 사용 예정이긴 한지라 곧 또 작업해야 해서 지금 같이 옮긴 것도 있긴 합니당) 요건 별도 PR에서 진행하겠습니다 🙏
| @transaction.atomic | ||
| def refund(self) -> None: |
There was a problem hiding this comment.
요것도 기능상 문제는 없어 보이지만, Serializer에서 직렬화 외의 실제 환불 작업을 담당하게 되면 직관적이지 않을 수도 있을 것 같아요.
Order 자체의 메소드로 빼거나, 아예 viewset에서 별도의 api를 만드는 것도 괜찮을 것 같습니다!
There was a problem hiding this comment.
요건 좀 고민되긴 합니다, refund를 model 메서드로 옮기면, PortOne HTTP 호출 + state machine 전이 + simple_history 기록까지 전부 옮겨가게 되는거라, ORM model이 외부 I/O 책임을 가지게 되어서 비대 패턴이 될 것 같거든요. 별도의 파일로 분리할 것이 아니라면 여기가 그나마 낫지 않을까......싶긴 합니다 🤔
작업 배경
이에 payment repo를 backend repo에 병합합니다.
주요 변경 사항
1.
purchase_shared→app/core흡수core/external_apis/portone/)core/util/)core/authn/api_key.py) + 권한 (core/authz/api_key.py)2.
python-korea-payment의 결제 도메인 →app/shop/*sub-appshop/order— Order, OrderProductRelation, OrderProductOptionRelation, SingleProductCart, CustomerInfoshop/product— Product, OptionGroup, Option, Tag, Category, CategoryGroupshop/payment_history— PortOne v1 webhook + 상태 전이 검증3. UserExt / 인증 통합
UserExt.unique_id추가 (QR scancode 식별자)google_oauth2와 병행)4. ScanCode (QR 코드) 도메인
core/scancode_mixin.ScanCodeMixin—UserExt/Order/OrderProductRelation공통shop/order/views/scancode.py단일 viewset, prefix dispatch (user:/order:/opr:)5. 어드민 (
admin_api/views/shop/)transaction.on_commitenqueue)6. 사용자 / 외부 API
internal_api/— desk_support (등록 데스크 환불 등)shop/patron.py— 후원자 목록 공개 API7. 보안 / 정합성
select_for_update)Orderaggregate root row lock)후속 작업 (별도 PR)
shop 도메인 + simple-history 복사. cutover 시
LEGACY_DATABASE_*env var 설정 후 실행.