12 minlesson

Delivery Confirmation Workflow

Delivery Confirmation Workflow

Delivery confirmation is the final milestone in a parcel's lifecycle. When a carrier marks a parcel as delivered, the system must record proof-of-delivery data, update the parcel status, create a tracking event, and calculate whether the delivery was on time. This lesson covers the concepts behind multi-step delivery workflows, proof-of-delivery data, and on-time delivery metrics.

What Is Delivery Confirmation?

Delivery confirmation is the process of recording that a parcel has been physically received. Unlike a simple status update, delivery confirmation captures structured evidence:

  • Who received it: The name of the person who accepted the package
  • Where it was left: A description of the delivery location (e.g., "Front door", "Left with neighbor", "Mailroom", "Reception desk")
  • Proof of delivery: An optional signature image captured as base64-encoded data
  • When it happened: The exact timestamp of delivery

Real-world carriers like UPS, FedEx, and USPS all capture similar data. If you have ever tracked a package and seen "Delivered - Signed by: JOHNSON", that information came from a delivery confirmation record.

The Multi-Step Workflow

Confirming a delivery is not a single database write. It is a coordinated series of operations that must succeed or fail together:

1POST /api/parcels/{trackingNumber}/delivery-confirmation
2
3
4 ┌───────────────────────┐
5 │ 1. Validate Status │ Parcel must be InTransit or OutForDelivery
6 └───────────┬───────────┘
7
8
9 ┌───────────────────────┐
10 │ 2. Check Duplicates │ Return 409 if confirmation already exists
11 └───────────┬───────────┘
12
13
14 ┌───────────────────────┐
15 │ 3. Create Record │ Insert DeliveryConfirmation entity
16 └───────────┬───────────┘
17
18
19 ┌───────────────────────┐
20 │ 4. Create Event │ Auto-create "Delivered" TrackingEvent
21 └───────────┬───────────┘
22
23
24 ┌───────────────────────┐
25 │ 5. Update Parcel │ Set status to Delivered, set ActualDeliveryDate
26 └───────────┘───────────┘

Each step depends on the previous one. If any step fails, the entire operation should roll back so the database remains in a consistent state.

Why Status Validation Matters

Not every parcel can be confirmed as delivered. A parcel with status LabelCreated has not even been picked up yet. A parcel with status Returned has already gone back to the sender. Allowing delivery confirmation on these parcels would create contradictory data.

The valid statuses for delivery confirmation are:

StatusWhy It Is Valid
InTransitThe parcel is moving through the network and could arrive at any time
OutForDeliveryThe parcel is on the delivery vehicle, the most common pre-delivery status

All other statuses should be rejected with a 400 Bad Request that explains why the confirmation cannot proceed.

Handling Duplicate Confirmations

A parcel can only be delivered once. If a delivery confirmation record already exists for a parcel, the API should return 409 Conflict instead of creating a duplicate.

The 409 status code communicates a specific situation: the request is valid in format, but it conflicts with the current state of the resource. This is different from 400 (bad request format) or 422 (validation failure). Using 409 tells the client exactly what happened so it can handle the situation appropriately.

Why Not Idempotent Creation?

Some APIs make POST endpoints idempotent: sending the same request twice returns the existing resource on the second call instead of an error. This simplifies retry logic for unreliable networks.

For delivery confirmation, 409 is the better choice. Delivery is a business event with real-world consequences. If a client receives a 409, it should fetch the existing confirmation and verify the data matches, rather than silently treating a duplicate as success. This prevents scenarios where two different drivers both claim to have delivered the same parcel with conflicting proof-of-delivery data.

Proof of Delivery: Base64 Signature Images

Delivery drivers often capture a recipient's signature on a handheld device. The signature is a small image, typically a PNG or JPEG, that gets encoded as a base64 string for transmission over HTTP.

Base64 encoding converts binary data into a text-safe format using 64 ASCII characters. A small signature image (say, 5 KB) becomes roughly 6.7 KB of base64 text. This is small enough to include directly in a JSON request body without needing multipart form uploads.

json
1{
2 "receivedBy": "Jane Smith",
3 "deliveryLocation": "Front door",
4 "signatureImage": "iVBORw0KGgoAAAANSUhEUgAA...",
5 "deliveredAt": "2025-02-15T14:30:00Z"
6}

In the database, the signature is stored as a string column. The API does not need to decode or process the image. It stores the base64 data as-is and returns it when the delivery confirmation is retrieved.

On-Time Delivery Metrics

Every parcel has an EstimatedDeliveryDate set when it was registered. When delivery is confirmed, the system can compare the actual delivery timestamp against the estimate to determine whether the delivery was on time.

The calculation is straightforward:

On Time = ActualDeliveryDate <= EstimatedDeliveryDate

This comparison uses the date portion only, ignoring time. A parcel estimated for February 15 that arrives at 11:59 PM on February 15 is on time. A parcel that arrives at 12:01 AM on February 16 is late.

Calculated vs. Stored Metrics

The on-time metric is not stored in the database. It is calculated at read time when the GET endpoint returns the delivery confirmation. This approach has two advantages:

  1. No data duplication: The on-time flag is always derived from the actual and estimated dates, so it cannot become stale or contradictory.
  2. Simplicity: The write path (POST endpoint) does not need to compute the metric. The read path (GET endpoint) calculates it on the fly.

The trade-off is a small computation on every GET request, but comparing two dates is trivial. If you later need to aggregate on-time rates across thousands of parcels, you could precompute and store the metric at write time, or use a database view or materialized query.

The GET Endpoint

The GET endpoint retrieves the delivery confirmation for a specific parcel. It returns the confirmation data along with the calculated on-time status:

GET /api/parcels/{trackingNumber}/delivery-confirmation

If no delivery confirmation exists for the parcel, the endpoint returns 404 Not Found. The response includes all the proof-of-delivery fields plus the computed isOnTime flag.

Transactional Consistency

The delivery confirmation workflow modifies three entities in a single request:

  1. A new DeliveryConfirmation record
  2. A new TrackingEvent record
  3. An updated Parcel entity (status and actual delivery date)

If the tracking event insert succeeds but the parcel update fails, the database would be inconsistent. The tracking history would show "Delivered" while the parcel status still says "OutForDelivery".

EF Core's SaveChangesAsync wraps all pending changes in a single database transaction by default. As long as you make all modifications before calling SaveChangesAsync, they either all succeed or all fail. This is exactly the behavior we need for the delivery confirmation workflow.

Summary

In this lesson, you learned:

  • Delivery confirmation is a multi-step workflow that creates a confirmation record, a tracking event, and updates the parcel status
  • Status validation ensures only InTransit or OutForDelivery parcels can be confirmed
  • Duplicate confirmations are rejected with 409 Conflict
  • Base64-encoded signature images are stored as strings, avoiding multipart form uploads
  • On-time delivery is calculated by comparing the actual delivery date to the estimated delivery date
  • EF Core's SaveChangesAsync provides transactional consistency for multi-entity writes

Next, we will build the POST endpoint that implements this workflow step by step.