예제 #1
0
    def cancel(payload: dict,
               carrier_filter: dict = None,
               carrier: models.Carrier = None) -> ConfirmationResponse:
        carrier = carrier or next(
            iter(Carriers.list(**{
                **(carrier_filter or {}), 'active': True
            })), None)
        print(carrier)
        if carrier is None:
            raise NotFound('No configured and active carrier found')

        request = purplship.Pickup.cancel(
            PickupCancelRequest(**DP.to_dict(payload)))
        gateway = purplship.gateway[carrier.data.carrier_name].create(
            carrier.data.dict())

        # The request call is wrapped in identity to simplify mocking in tests
        confirmation, messages = identity(
            lambda: request.from_(gateway).parse())

        if confirmation is None:
            raise PurplShipApiException(
                detail=ErrorResponse(messages=messages),
                status_code=status.HTTP_400_BAD_REQUEST)

        return ConfirmationResponse(confirmation=confirmation,
                                    messages=messages)
예제 #2
0
    def track(payload: dict,
              carrier_filter: dict = None,
              carrier: models.Carrier = None) -> TrackingResponse:
        carrier = carrier or next(
            iter(Carriers.list(**{
                **(carrier_filter or {}), 'active': True
            })), None)

        if carrier is None:
            raise NotFound('No configured and active carrier found')

        request = purplship.Tracking.fetch(
            TrackingRequest(**DP.to_dict(payload)))
        gateway = purplship.gateway[carrier.data.carrier_name].create(
            carrier.data.dict())

        # The request call is wrapped in identity to simplify mocking in tests
        results, messages = identity(lambda: request.from_(gateway).parse())

        if any(messages or []) and not any(results or []):
            raise PurplShipApiException(
                detail=ErrorResponse(messages=messages),
                status_code=status.HTTP_404_NOT_FOUND)

        return TrackingResponse(tracking=(Tracking(
            **{
                **DP.to_dict(results[0]),
                'id': f'trk_{uuid.uuid4().hex}',
                'test_mode': carrier.test,
            }) if any(results) else None),
                                messages=messages)
예제 #3
0
    def schedule(payload: dict,
                 carrier_filter: dict = None,
                 carrier: models.Carrier = None) -> PickupResponse:
        carrier = carrier or next(
            iter(Carriers.list(**{
                **(carrier_filter or {}), 'active': True
            })), None)

        if carrier is None:
            raise NotFound('No configured and active carrier found')

        request = purplship.Pickup.schedule(
            PickupRequest(**DP.to_dict(payload)))
        gateway = purplship.gateway[carrier.data.carrier_name].create(
            carrier.data.dict())

        # The request call is wrapped in identity to simplify mocking in tests
        pickup, messages = identity(lambda: request.from_(gateway).parse())

        if pickup is None:
            raise PurplShipApiException(
                detail=ErrorResponse(messages=messages),
                status_code=status.HTTP_400_BAD_REQUEST)

        return PickupResponse(pickup=Pickup(
            **{
                **payload,
                **DP.to_dict(pickup),
                'id': f'pck_{uuid.uuid4().hex}',
                'test_mode': carrier.test,
            }),
                              messages=messages)
예제 #4
0
    def post(self, request: Request, pk: str):
        """
        Select your preferred rates to buy a shipment label.
        """
        shipment = request.user.shipment_set.get(pk=pk)

        if shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException(
                f"The shipment is '{shipment.status}' and therefore already {ShipmentStatus.purchased.value}",
                code='state_error',
                status_code=status.HTTP_409_CONFLICT)

        payload = {
            **Shipment(shipment).data,
            **SerializerDecorator[ShipmentPurchaseData](data=request.data).data
        }

        # Submit shipment to carriers
        response: Shipment = SerializerDecorator[ShipmentValidationData](
            data=payload).save(user=request.user).instance

        # Update shipment state
        SerializerDecorator[ShipmentSerializer](
            shipment, data=DP.to_dict(response)).save()

        return Response(Shipment(shipment).data)
예제 #5
0
    def post(self, request: Request, pk: str):
        """
        Add one or many options to your shipment.<br/>
        **eg:**<br/>
        - add shipment **insurance**
        - specify the preferred transaction **currency**
        - setup a **cash collected on delivery** option

        ```json
        {
            "insurance": 120,
            "currency": "USD"
        }
        ```

        And many more, check additional options available in the [reference](#operation/all_references).
        """
        shipment = request.user.shipment_set.get(pk=pk)

        if shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException("Shipment already 'purchased'",
                                        code='state_error',
                                        status_code=status.HTTP_409_CONFLICT)

        payload: dict = DP.to_dict(dict(options=request.data))

        SerializerDecorator[ShipmentSerializer](shipment, data=payload).save()
        reset_related_shipment_rates(shipment)
        return Response(Shipment(shipment).data)
예제 #6
0
    def create(payload: dict,
               resolve_tracking_url: Callable[[Shipment], str] = None,
               carrier: models.Carrier = None) -> Shipment:
        selected_rate = next(
            (Rate(**rate) for rate in payload.get('rates')
             if rate.get('id') == payload.get('selected_rate_id')), None)

        if selected_rate is None:
            raise NotFound(
                f'Invalid selected_rate_id "{payload.get("selected_rate_id")}" \n '
                f'Please select one of the following: [ {", ".join([r.get("id") for r in payload.get("rates")])} ]'
            )

        carrier = carrier or Carriers.retrieve(
            carrier_id=selected_rate.carrier_id).data
        request = ShipmentRequest(
            **{
                **DP.to_dict(payload), 'service': selected_rate.service
            })
        gateway = purplship.gateway[carrier.carrier_name].create(
            carrier.dict())

        # The request is wrapped in identity to simplify mocking in tests
        shipment, messages = identity(
            lambda: purplship.Shipment.create(request).from_(gateway).parse())

        if shipment is None:
            raise PurplShipApiException(
                detail=ErrorResponse(messages=messages),
                status_code=status.HTTP_400_BAD_REQUEST)

        shipment_rate = ({
            **DP.to_dict(shipment.selected_rate), 'id':
            f'rat_{uuid.uuid4().hex}'
        } if shipment.selected_rate is not None else DP.to_dict(selected_rate))

        def generate_tracking_url():
            if resolve_tracking_url is None:
                return ''

            return f"{resolve_tracking_url(shipment)}{'?test' if carrier.test else ''}"

        return Shipment(
            **{
                **payload,
                **DP.to_dict(shipment), "id": f"shp_{uuid.uuid4().hex}",
                "test_mode": carrier.test,
                "selected_rate": shipment_rate,
                "service": shipment_rate["service"],
                "selected_rate_id": shipment_rate["id"],
                "tracking_url": generate_tracking_url(),
                "status": ShipmentStatus.purchased.value,
                "created_at": datetime.now().strftime(
                    "%Y-%m-%d %H:%M:%S.%f%z"),
                "messages": messages
            })
예제 #7
0
    def post(self, request: Request, pk: str):
        """
        Add the customs declaration for the shipment if non existent.
        """
        shipment = request.user.shipment_set.get(pk=pk)

        if shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException("Shipment already 'purchased'",
                                        code='state_error',
                                        status_code=status.HTTP_409_CONFLICT)

        if shipment.customs is not None:
            raise PurplShipApiException(
                "Shipment customs declaration already defined",
                code='state_error',
                status_code=status.HTTP_409_CONFLICT)

        SerializerDecorator[ShipmentSerializer](
            shipment, data=dict(customs=request.data)).save()
        reset_related_shipment_rates(shipment)
        return Response(Shipment(shipment).data)
예제 #8
0
    def patch(self, request: Request, pk: str):
        """
        modify an existing parcel's details.
        """
        parcel = request.user.parcel_set.get(pk=pk)
        shipment = parcel.shipment_parcels.first()
        if shipment is not None and shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException(
                "The shipment related to this parcel has been 'purchased' and can no longer be modified",
                status_code=status.HTTP_409_CONFLICT,
                code='state_error')

        SerializerDecorator[ParcelSerializer](parcel, data=request.data).save()
        reset_related_shipment_rates(shipment)
        return Response(Parcel(parcel).data)
예제 #9
0
    def delete(self, request: Request, pk: str):
        """
        Void a shipment with the associated label.
        """
        shipment = request.user.shipment_set.get(pk=pk)

        if shipment.status not in [
                ShipmentStatus.purchased.value, ShipmentStatus.created.value
        ]:
            raise PurplShipApiException(
                f"The shipment is '{shipment.status}' and can therefore not be cancelled anymore...",
                code='state_error',
                status_code=status.HTTP_409_CONFLICT)

        if shipment.pickup_shipments.exists():
            raise PurplShipApiException((
                f"This shipment is scheduled for pickup '{shipment.pickup_shipments.first().pk}' "
                "Please cancel this shipment from the pickup before."),
                                        code='state_error',
                                        status_code=status.HTTP_409_CONFLICT)

        confirmation = SerializerDecorator[ShipmentCancelSerializer](
            shipment, data={}).save()
        return Response(OperationResponse(confirmation.instance).data)
예제 #10
0
    def patch(self, request: Request, pk: str):
        """
        update an address.
        """
        address = request.user.address_set.get(pk=pk)
        shipment = address.shipper.first() or address.recipient.first()
        if shipment is not None and shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException(
                "The shipment related to this address has been 'purchased' and can no longer be modified",
                status_code=status.HTTP_409_CONFLICT,
                code='state_error'
            )

        SerializerDecorator[AddressSerializer](address, data=request.data).save()
        reset_related_shipment_rates(shipment)
        return Response(Address(address).data)
예제 #11
0
    def patch(self, request: Request, pk: str):
        """
        modify an existing customs declaration.
        """
        customs = request.user.customs_set.get(pk=pk)
        shipment = customs.shipment_set.first()
        if shipment is not None and shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException(
                "The shipment related to this customs info has been 'purchased' and can no longer be modified",
                status_code=status.HTTP_409_CONFLICT,
                code='state_error')

        SerializerDecorator[CustomsSerializer](
            customs, data=request.data).save(user=request.user)
        reset_related_shipment_rates(shipment)
        return Response(Customs(customs).data)
예제 #12
0
    def post(self, request: Request, pk: str):
        """
        Add a parcel to an existing shipment for a multi-parcel shipment.
        """
        shipment = request.user.shipment_set.get(pk=pk)

        if shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException("Shipment already 'purchased'",
                                        code='state_error',
                                        status_code=status.HTTP_409_CONFLICT)

        parcel = SerializerDecorator[ParcelSerializer](data=request.data).save(
            user=request.user).instance
        shipment.shipment_parcels.add(parcel)
        reset_related_shipment_rates(shipment)
        return Response(Shipment(shipment).data)
예제 #13
0
    def fetch(payload: dict, user=None) -> RateResponse:
        request = purplship.Rating.fetch(RateRequest(**DP.to_dict(payload)))

        carrier_settings_list = [
            carrier.data for carrier in Carriers.list(carrier_ids=payload.get(
                'carrier_ids', []),
                                                      active=True,
                                                      user=user)
        ]
        gateways = [
            purplship.gateway[c.carrier_name].create(c.dict())
            for c in carrier_settings_list
        ]
        compatible_gateways = [
            g for g in gateways if 'get_rates' in g.features
        ]

        if len(compatible_gateways) == 0:
            raise NotFound("No configured and active carriers specified")

        # The request call is wrapped in identity to simplify mocking in tests
        rates, messages = identity(
            lambda: request.from_(*compatible_gateways).parse())

        if not any(rates) and any(messages):
            raise PurplShipApiException(
                detail=ErrorResponse(messages=messages),
                status_code=status.HTTP_400_BAD_REQUEST)

        def consolidate_rate(rate: Rate) -> Rate:
            carrier = next((c for c in carrier_settings_list
                            if c.carrier_id == rate.carrier_id))
            return Rate(
                **{
                    'id': f'rat_{uuid.uuid4().hex}',
                    'carrier_ref': carrier.id,
                    'test_mode': carrier.test,
                    **DP.to_dict(rate)
                })

        rates: List[Rate] = sorted(map(consolidate_rate, rates),
                                   key=lambda rate: rate.total_charge)

        return RateResponse(rates=sorted(rates,
                                         key=lambda rate: rate.total_charge),
                            messages=messages)
예제 #14
0
    def delete(self, request: Request, pk: str):
        """
        Discard a customs declaration.
        """
        customs = request.user.customs_set.get(pk=pk)
        shipment = customs.shipment_set.first()
        if shipment is not None and shipment.status == ShipmentStatus.purchased.value:
            raise PurplShipApiException(
                "The shipment related to this customs info has been 'purchased' and cannot be discarded",
                status_code=status.HTTP_409_CONFLICT,
                code='state_error')

        customs.delete(keep_parents=True)
        shipment.customs = None
        serializer = Operation(
            dict(operation="Discard customs info", success=True))
        reset_related_shipment_rates(shipment)
        return Response(serializer.data)
예제 #15
0
    def validate(payload: dict, carrier_filter: dict) -> AddressValidation:
        carrier = next(iter(Carriers.list(**(carrier_filter or {}))), None)

        if carrier is None:
            raise NotFound('No configured and active carrier found')

        request = purplship.Address.validate(
            AddressValidationRequest(**DP.to_dict(payload)))
        gateway = purplship.gateway[carrier.data.carrier_name].create(
            carrier.data.dict())

        # The request call is wrapped in identity to simplify mocking in tests
        validation, messages = identity(lambda: request.from_(gateway).parse())

        if validation is None:
            raise PurplShipApiException(
                detail=ErrorResponse(messages=messages),
                status_code=status.HTTP_400_BAD_REQUEST)

        return AddressValidation(validation=validation, messages=messages)
예제 #16
0
    def delete(self, request: Request, pk: str):
        """
        Remove a parcel.
        """
        parcel = request.user.parcel_set.get(pk=pk)
        shipment = parcel.shipment_parcels.first()

        if shipment is not None and (
                shipment.status == ShipmentStatus.purchased.value
                or len(shipment.shipment_parcels.all()) == 1):
            raise PurplShipApiException(
                "A shipment attached to this parcel is purchased or has only one parcel. The parcel cannot be removed!",
                status_code=status.HTTP_409_CONFLICT,
                code='state_error')

        parcel.delete(keep_parents=True)
        shipment.shipment_parcels.set(
            shipment.shipment_parcels.exclude(id=parcel.id))
        serializer = Operation(dict(operation="Remove parcel", success=True))
        reset_related_shipment_rates(shipment)
        return Response(serializer.data)