Payton.Codes

Musings on life, games, and code

The Receipt Item Tag Trick

I’m not sure I should be proud of this solution, but I wanted to discuss a technique I invented to solve a tricky problem at work. Our billing receipt database records have a separate table called “receipt tags” which are just simple strings that can be associated with a receipt. These are useful for all sorts of meta things like the source of the receipt, whether a receipt was a gift purchase, or the remote transaction ID. However, the receipt items – the individual line items on each receipt – do not have any tag table of their own.

How to record data without a new table

The problem I encountered was that I wanted to start recording line item discounts on our new purchase receipts so that it was clear why a receipt item had the price we recorded. This data involved a number of fields:

  • The discount code.
  • The price prior to the discount.
  • The price after the discount.
  • The type of the discount (percentage, total, first unit of a multi-unit product, etc.)
  • The discount percentage (if it was based on a percentage).

It seemed like a big ask to request a whole new database table just to record this meta information. Especially because I knew that there was other line item metadata that would also be useful to store, like the number of units of an item, the amount of credits used on that item, etc. The table would have to support all sorts of different data so it would need some sort of scheme to handle a variable number of fields. Was there some way I could use our existing database tables?

Ultimately, I came up with a bit of a hack which worked surprisingly well.

Making an ID

Part of the challenge was that while we knew all the metadata when the line items were in the shopping cart, by the time the receipt was being written, the items had gone through several other parts of the billing system and there was no easy way to identify which of the resulting receipt line items matched the shopping cart item that had created it. This led me to look at what uniquely identified both a shopping cart item and a receipt line item; how could I, as a human, tell that they were for the same thing?

Here’s the criteria I finally used: the product’s billing plan ID (the billing plan is what describes the subscription price and renewal period), the item’s total cost (after discounts and taxes), and the domain name for that product, if any.

For example, we might have an annual domain registration – billing plan 1234 – that cost $24 for the domain “example.com”. Looking at the receipt and the shopping cart, you’d be able to pick out this unique item quite easily.

Great! I had a way to identify a receipt item. But how do I use those properties to store the metadata I need to record?

I already knew that we had receipt tags. Was there a way I could put this all info into a single tag? It would not be easy; domain names alone could be quite long and it would be very easy to hit the character limit of the tag. Then I decided I didn’t really need to record all those pieces of information themselves; I just needed a way to uniquely identify a line item in a way that I could create from the shopping cart and later use to find on a receipt. I could hash the data together to create a key!

function get_id_with_product_data_for_receipt_item(
	Product_Data_For_Receipt_Item_Id $data,
): string {
	return hash(
		'crc32b',
		"$data->billing_plan_id:$data->item_subtotal_integer:$data->domain",
	);
}

With the example above, we’d end up calling hash('crc32b', "1234:2400:example.com") which returns 68aa5c5d. That’s our ID.

Since these hashes could never be regenerated (the original shopping cart data that created them was gone), I also made sure to version the ID to make sure that it could support any future alterations that were needed:

function get_id_with_product_data_for_receipt_item(
	int $version,
	Product_Data_For_Receipt_Item_Id $data,
): string {
	if ($version === 1) {
		$hashed_id = hash(
			'crc32b',
			"$data->billing_plan_id:$data->item_subtotal_integer:$data->domain",
		);
		return "v{$version}={$hashed_id}";
	}
	throw new \Exception(
		"Unknown version for get_id_with_product_data_for_receipt_item '{$version}'",
	);
}

And it worked: the billing plan, total, and domain name could be hashed to create a line item ID (using a hashing algorithm that generated a relatively short string). Then I just needed to use this ID to store the data fields.

Encoding a receipt item tag

I started by making the ID for the shopping cart item, and then I appended the rest of the line item metadata in colon-separated fields, like v1=68aa5c5d:sale-discount:42:37.8:10 which would be a 10% sale discount from $42 to $37.80 (the actual strings have a few more fields but that’s the basics). The resulting string was inserted into the receipt tags table, and could be decoded later.

// Create the line item ID
$product_data = new \Product_Data_For_Receipt_Item_Id();
$product_data->billing_plan_id =
	$cart_item->billing_plan_id;
$product_data->item_subtotal_integer =
	$cart_item->item_subtotal_integer;
$product_data->domain = $cart_item->domain;
$item_id = get_id_with_product_data_for_receipt_item(
	1,
	$product_data,
);

// Use the ID to create tags for each discount
$tags = [];
foreach (
	$cart_item->cost_overrides as $override
) {
	$discount = new \Cost_Override_For_Receipt_Item();
	$discount->id = $item_id;
	$discount->override_code = $override['override_code'];
	$discount->old_price = (float) $override['old_subtotal'];
	$discount->new_price = (float) $override['new_subtotal'];
	// ...
	$tags[] = encode_receipt_item_discount_tag($discount);
}
function encode_receipt_item_discount_tag( Cost_Override_For_Receipt_Item $override ): string {
	return RECEIPT_ITEM_DISCOUNT_PREFIX . "{$override->id}:{$override->order}:{$override->override_code}:{$override->old_price}:{$override->new_price}:{$override->override_type}:{$override->percentage}";
}

Decoding the tag

To display or use the data we stored, we would do the same thing in reverse, starting from the data already stored in the receipt.

First, we’d use the information we had about each receipt item to generate a hash key which should be identical to the one we created from the shopping cart item. Then we could use that key to identify and decode the receipt item tag. I wrote a helper class to make this easier:

/**
 * A helper class to parse receipt tags that belong to a receipt item.
 */
final class Receipt_Item_Tags
{
	/**
	 * @var string[] The tags that belong to this receipt item.
	 */
	private array $receipt_item_tags = [];

	/**
	 * Collect the tags on a receipt that reference a specific receipt item.
	 *
	 * @param string[] $all_tags All the tags on the receipt.
	 * @param Product_Data_For_Receipt_Item_Id $receipt_item The structured receipt item data.
	 * @return string[] The tags that belong to this receipt item.
	 */
	public function __construct(
		array $all_tags,
		Product_Data_For_Receipt_Item_Id $receipt_item,
	) {
		$this->receipt_item_tags = $this->filter_tags_for_receipt_item(
			$all_tags,
			$receipt_item,
		);
	}

	/**
	 * Return the cost overrides used on a receipt item based on the receipt's
	 * tags.
	 *
	 * @return Cost_Override_For_Receipt_Item[]
	 */
	public function get_cost_overrides(): array
	{
		$discounts = array_values(
			array_filter(
				array_map(
					fn(
						string $tag,
					) => parse_receipt_item_discount_tag($tag),
					$this->receipt_item_tags,
				),
			),
		);
		usort(
			$discounts,
			fn(
				\Cost_Override_For_Receipt_Item $a,
				\Cost_Override_For_Receipt_Item $b,
			) => $a->order <=> $b->order,
		);
		return $discounts;
	}

	/**
	 * Find the tags on a receipt that reference a specific receipt item.
	 *
	 * @param string[] $all_tags All the tags on the receipt.
	 * @param Product_Data_For_Receipt_Item_Id $receipt_item The structured receipt item data.
	 * @return string[] The tags that belong to this receipt item.
	 */
	private function filter_tags_for_receipt_item(
		array $all_tags,
		Product_Data_For_Receipt_Item_Id $receipt_item,
	): array {
		return array_values(
			array_filter(
				$all_tags,
				fn(
					string $tag,
				): bool => $this->does_tag_belong_to_receipt_item(
					$tag,
					$receipt_item,
				),
			),
		);
	}

	private function does_tag_belong_to_receipt_item(
		string $tag,
		Product_Data_For_Receipt_Item_Id $receipt_item,
	): bool {
		if ($this->is_discount_tag($tag)) {
			$cost_override = parse_receipt_item_discount_tag(
				$tag,
			);
			return $cost_override
				? $this->does_receipt_item_tag_id_belong_to_receipt_item(
					$cost_override->id,
					$receipt_item,
				)
				: false;
		}
		// ... other types of tags here ...
		return false;
	}

	private function does_receipt_item_tag_id_belong_to_receipt_item(
		string $receipt_item_tag_id,
		Product_Data_For_Receipt_Item_Id $receipt_item,
	): bool {
		$item_id_version = get_version_for_product_data_id(
			$receipt_item_tag_id,
		);
		$this_item_id = get_id_with_product_data_for_receipt_item(
			$item_id_version,
			$receipt_item,
		);
		return $this_item_id === $receipt_item_tag_id;
	}

	private function is_discount_tag(string $tag): bool
	{
		return wp_startswith(
			$tag,
			RECEIPT_ITEM_DISCOUNT_PREFIX,
		);
	}
}

As an example use-case, let’s say we wanted to write a function to determine if a sale price was applied to a receipt item, so that we can display “Sale Discount” on the receipt. We could write a function like the following to search the receipt tags for a sale discount matching the receipt item:

function was_receipt_item_bought_on_sale(Receipt_Item $receipt_item): bool {
	$cost_overrides = (new \Receipt_Item_Tags(
		get_all_tags_for_receipt($receipt_item->receipt_id),
		generate_receipt_item_data($receipt_item),
	))->get_cost_overrides();
	foreach ($cost_overrides as $override) {
		if ($override->override_code === 'sale-discount') {
			return true;
		}
	}
	return false;
}

Was it the right solution?

You can see the full version of the code to perform these operations here. Was this better than using a new database table? I’m not sure.

It is a very flexible and powerful system, and I’ve been able to use it to encode all sorts of metadata for receipt items which has greatly improved the accuracy of our billing logic and our ability to trace how a price was generated.

However, it’s also somewhat fragile. Even though receipts are meant to be immutable, it would be very easy for something to change after the ID had been created which would prevent the receipt item hash from matching the shopping cart item hash. Despite my efforts, the tag length could still end up too long for the database field. Furthermore, it’s possible, however unlikely, that two items on the same receipt might have the same hash key.

Sometimes, we have to pick the best solution that we can at the time, and this was the one that worked when I needed it. I actually am proud of it. The things it’s enabled my team to accomplish were worth the trade-offs, at least for now. Some day maybe I’ll think of a better idea.

Photo by D J on Unsplash


Comments

Leave a comment