Weekly Roundup: Apr 18, 2025

Working for a small agency I am fortunate to work on a number of fast moving projects simultaneously. For years I’ve failed to document what I do during the week but I’m going a little recap of my week. One part historical record, one part general interest. I’m posting it on my blog in the off chance that somebody reads it and, facing a similar problem will reach out I’m always happy to discuss what worked for me and what didn’t work. It also doesn’t hurt to put this stuff into the world to show that yes I actually do work; I haven’t always had the most active GitHub but most of my client projects a private/propriety. I’m easing into this, all week I was looking forward to this post; now, however, I realize I should have been working on this not cramming it in from memory on a Friday night.

This week was a balance between my ongoing Elixir projects and a newer (to me) Ruby project.

  • For the past five years I’ve either supported, or been the lead dev on a large B2B ecommerce platform which handles a few million in daily sales. Over the winter the company began consolidating their North American and European processes which includes using said platform for sales in the EU. Although the hope is that the European process will align with the North American there are some relevant differences. For example in North America the client’s product is technically considered a “raw material” which means there is no “Value Added Tax” (VAT); however in Europe, depending on the country of origin and the destination VAT may be charged, other relevant changes are shipping across borders, truck loading calculations and different invoicing procedures. At this point we are still in the research and discovery phase but I’ve been working with another developer to scope this project out and write some preliminary tests as research.
  • For another client I’ve been moving from a Quickbooks Online integration to Quickbooks Desktop, this is a multi-tenancy Elixir Phoenix app so I’ll be keeping the Online functionality and just adding a connection to Quickbooks Desktop. The API docs for QBOnline are fairly good, this is not the case with QB Desktop, it’s evident that Intuit either has the platform on life support or intentionally obfuscates the functionality to foster a consulting industry around the product. QB Desktop uses an SOAP XML type endpoint. Having wrangled fairly nasty endpoints with SAP I wanted to, if at all possible, avoid dealing directly with QB Desktop. I discovered a service called Conductor that does the bulk of the heavy lifting and allows you to hit a very concise REST endpoint.
  • Since the beginning of the year I’ve been transitioning from primarily Elixir projects at the agency to a single Ruby based product. On that front I’ve been involved in an ongoing integration with BambooHR; partnering with Bamboo to pull employee data from their endpoint.
  • On a personal front I finished the migration of this blog from Ghost back to markdown files. I still love Ghost but managing my own instance and integrating it with my Garden proved to be more management than I wanted.

Experience has shown that if you put out a bug bounty your server will be hit repeatedly with requests to /wp_admin for the rest of eternity.

Personal Heuristic: Make it Readable

I wrote this post back in January, just dusted it off to post today as I attempt to get back on the blogging horse.


Today I was refactoring a small module that makes calls to an SAP endpoint. The compiler got hung up because it couldn’t find the value item. It was an easy fix, my code looked like this:

for itm <- data do
    %{"MATNR" => material, "PSTYV" => category, "VBELN" => so} = item
    %{material: material, category: category, so: so}
end

It’s easy to spot (especially if the compiler tells you exactly where it is); in the function head I wrote itm but down below I’m looking for item. Simple; yet this is not the first time something similar has happened to me. It’s also not the first time I’ve specifically confused itm with item which led me to this conclusion: just write item every time. There is an odd switch in my brain that thinks I’m penalized by the character, and leaving e out of item will somehow make my code more terse. While technically true, it’s not worth it. It never is; just write item, everytime. People know what item is. itm is more ambiguous, not just because it only saves one letter, but it could be an abbreviation or some weird naming convention. Why put that mental load on someone, even yourself, reading through this code? This is a tiny example but it’s magnified in function names. While check_preq may be quick to type and take up less horizontal space in an editor it’s not immediately clear what this function does. I would argue that get_purchase_requisition_number is a much better function name; even if you know nothing about the function, the codebase, or programming in general you can read that and know what’s supposed to happen. Of course there are conventions, ie. ! dangerous or ? bankbook method endings in Ruby ie. exitst? will throw an error. These sorts of things require one to be a little familiar with the patterns of a language but that’s ok that just means that I can write a function get_purchase_requisition_number! and anyone familiar with Ruby or Elixir will expect the function to raise or return an explicit value (as opposed to something wrapped in an :ok tuple).

Moving forward I’m calling things what they are even if it comes with a dash of verbosity.

Wintering: The Power of Rest and Retreat in Difficult Time

Reading 80% of this book was an exercise in torture. I’m always a little wary of personal memoirs cum self-help books but a few have been transformative for me (ie. Pamela Druckerman’s Bringing Up Bébé). Katherine May hooked me early with this book, the prose was sharp and the anecdotes interesting however it very quickly devolved into anecdote after anecdote from a brief period in her life where, I guess, she was forced to work less?

This book is rife with privilege, which doesn’t always bother me but in this particular case it seems to hallow where half of of the book is dedicated to the message of “slow down, take it easy” and the other half is, “go to Iceland, trek the northern tundras of Sweden”.

Ultimately this book fell into the same trap as Gretchen Rubin’s Happier At Home (a did not finish from last year); an extremely self absorbed upper-middle class person thinking that their experience = wisdom and is therefore worth writing an entire book.

Zero stars. Did not finish.

Today I Learned ~D[2025-01-10]

Today’s TIL has a twist ending… so stick around.

Elixir has a shortcut for creating anonymous functions. I’ve always written:

greet = fn name -> "Hello, #{name}!" end 
# which can be invoked
greet.("Travis")
# returns
"Hello, Travis!"

However; I came across some tricky code with a case statement:

docs = case type do 
	:billing -> &[billing_act: &1]
    :shipping -> &[shipping_act: &1]
end 

# invocation
type = :billing
docs.("some customer")
# returns 
[billing_act: "some customer"]

This was very confusing to me, the fact that the anonymous function was a case form only further obfuscated what was happening. I thought it might be some case magic.

No. Apparently you can short cut the aforementioned anonymous function declaration:

greet = & "Hello, #{&1}!"

You treat this as any other anonymous function. You can even have multi-arity functions:

greet = & "Hello, #{&1} #{&2}!"
# invocation 
greet.("Travis", "Fantina")
# returns 
"Hello, Travis Fantina!"

In my case the case statement could have also been written:

docs = fn customer -> 
	case type do
		:billing -> [billing_act: customer]
    	:shipping -> [shipping_act: customer]
	end
end

Plot twist: This is not a TIL, apparently I learned this at least four years ago. That initial case function… the author was me four years ago!

Canada Post Strike

Canada Post Strike

Photo by Birk Enwald

For the past few weeks postal workers in Canada have been on strike. This has, of course, caused a fair amount of disruption during the Christmas season. Before I even knew the details of the strike I was naturally on the side of the workers. This is my default position: support labour. In general, I have found that if you are on the side of labour you’re on the right side of history. That’s not to say there aren’t corrupt unions, pointless strikes, or strikes that are regressive in their outcomes. However, in general strikes are a way for “the means of production” to get more of the benefits of that production. In other words; the people doing the work should be reaping the biggest rewards. Not the government, not billionaires, not some mythological “job creator” who took some risks a few years ago and has sat around collecting a “passive income” ever since. No, labor deserves the wealth, because they are the ones creating it.

The arguments against strikes have always been a bit baffling to me. There is no multi-millionaire union. The picket line almost always represents the low and middle class. These are our people, and they deserve more for what they do. Even highly paid people, in my view, deserve more for what they do. In Ontario it’s taken for granted that public teachers are “highly paid”. OTF, the Ontario Teachers Federation is very powerful and well monied. So what are these rich teachers making? After 10 years salaries appear to be capped at $102,000*. Great money for a single person, maybe more than you make, maybe more than you take home but I would ask; “why not more?” Teachers work damn hard, long hours, lots of stress and there is the years-long grind of even becoming a full time placed teacher in Ontario. Not to mention these are people who spend as much time with your kids as you do! Yes, I also think you should make more money, but it’s not a zero sum game; that’s kind of the point of unions. We are living through a time when the wealth gap looks more like France before the revolution; the middle class is shrinking and not because families are getting rich. A “high paying” job ten years ago may barely get you by; but billionaires own more than ever. In the past 25 years the world’s billionaires have added six trillion to their collective wealth. Anybody, any union, who takes a stand is fighting for all of us not just their workers.

Sure it’s an inconvenient time for a strike, but spare a thought. You may not get all your Christmas packages in time, but there are workers who literally cannot make ends meet 365 days a year, not just at Christmas. Strong labour is good for society as a whole!

Not to rush Christmas, but I think I’ll try my hand at Advent of Code this year. It will be a good chance to play around with Rust.

Adding a `soft_delete` to Ecto Multi pipelines

I’m a big fan of Ecto, Elixir’s database wrapper. The Multi library lets you build up a series of operations that happen in order, if one fails the entire operation rolls back. Multi comes with the a lot of standard CRUD built in, insert/4 , update/4 , delete/4 and their bulk counterparts insert_all/5 , update_all/5 and delete_all/5 for acting on multiple records.

I’ve been working on a project where we make use of the soft delete pattern, rather than calling delete/4 on a record we generally update/4 the record passing in a deleted_at timestamp:

|> Multi.update(:soft_delete, fn %{customer: customer} -> 
	Changeset.change(customer, %{deleted_at: now})
end)

This works fine, and even updating multiple records one could take this approach:

|> Multi.update_all(:soft_delete, fn %{customers: customers} ->
	ids = Enum.map(customers, & &1.id)
	from(c in Customer, where: c.id in ^ids, update: [set: [deleted_at: ^now]])
end, [])

I was working on a new feature that will require a cascade of soft deletes, deleting multiple records, their associated records, their children, etc. (As the second example above is doing). Admittedly, I could have just utilized this Multi.update_all/5 and put multiple steps into the multi . However; I thought continuously mapping specific ids, passing in set: [deleted_at: ^now] was a little cumbersome and not very idiomatic. Mostly, I wanted to have a bit of fun wondering: “what if Ecto.Multi had a soft_delete_all/5 function?” Of course it doesn’t, this is a niche use case but it makes sense in this application so I dug in and found the task to be (as is the case with a lot of Elixir) surprisingly easy.

Just like update_all/5 I wanted to make sure soft_delete_all would handle queries or functions passed in. Pattern matching here using the is_function/1 guard. This made it a fairly straightforward operation:

@spec soft_delete_all(Multi.t(), atom(), fun() | Query.t(), keyword()) :: Multi.t()
  def soft_delete_all(multi, name, func, opts \\ [])

  def soft_delete_all(multi, name, func, opts) when is_function(func) do
    Multi.run(
      multi,
      name,
      operation_fun({:soft_delete_all, func, [set: [deleted_at: Timex.now()]]}, opts)
    )
  end

  def soft_delete_all(multi, name, queryable, opts) do
    add_operation(multi, name, {:update_all, queryable, [set: [deleted_at: Timex.now()]], opts})
  end

The first function matches against functions while the second matches against a queryable. I’ll explain the distinction between both.

Under the hood Multi is already equipped to handle functions or queryables; by reading the source of the Multi module I was able to,matches, forward along the proper structure for the Multi to run, and in another case recreate the same functionality that Multi.update_all uses. Both operation_fun/2 and add_operation/3 are nearly copy-pasted from the Multi core.

In the first instance the multi is passed a function, something like:

|> soft_delete_all(:remove_customer, &remove_customer/1)

In this case Ecto adds a new Multi operation to the pipeline: Multi.run/3 but it needs to run the function it’s passed. It does this with operation_fun/2 . The multi has several matchers for each of the bulk operations, in my case I only needed one :soft_delete_all .

defp operation_fun({:soft_delete_all, fun, updates}, opts) do
    fn repo, changes ->
      {:ok, repo.update_all(fun.(changes), updates, opts)}
    end
  end

Again, this is identical (save the :soft_delete_all atom) to the Multi module. It runs our function which creates a query, it passes our update: [set: [deleted_at: Timex.now()]] to the query and then updates the record.

In cases where we pass a query in:

|> soft_delete_all(:remove_customer, Query.from(c in Customer, where: c.id == 123))

We match on the next function head, here again I used Ecto’s pattern writing my own custom add_operation/3

defp add_operation(%Multi{} = multi, name, operation) do
    %{operations: operations, names: names} = multi

    if MapSet.member?(names, name) do
      raise "#{Kernel.inspect(name)} is already a member of the Ecto.Multi: \n#{Kernel.inspect(multi)}"
    else
      %{multi | operations: [{name, operation} | operations], names: MapSet.put(names, name)}
    end
  end

This is going to first check that the operation name isn’t already in the Multi. If it’s not, we append the operation into the Multi. This works because of the parameters we’ve passed it:

add_operation(multi, name, {:update_all, queryable, [set: [deleted_at: Timex.now()]], opts})
  end

Specifically: {:update_all, queryable, [set: [deleted_at: Timex.now()]], opts} once again, we aren’t doing anything fancy to soft delete these records, we are using Multi’s ability to :update_all with our provided queryable. The update we are making is [set: [deleted_at: Timex.now()]] .

There you have it, it’s :update_all all the way down, which makes sense because we are updating a record instead of deleting it, but I think it’s a lot cleaner to write something like this:

query1 = from(c in Customer, where: c.last_purchase <= ^old_date)
query2 = from(u in User, join: c in assoc(u, :customer), on: c.last_purchase <= ^old_date)

Multi.new()
|> soft_delete_all(:customers, query1)
|> soft_delete_all(:users, query2)
#👆don't judge this contrived example it's not production code