Bus Pirate shop

We’re supposed to have our own shop here, but I’ve been putting it off for months. I’ve worked with Salor (Python), MedusaJS (Node), Sylius (PHP), and Vendure (Node). These are all next generation headless eCommerce backends that will let us customize like crazy.

The goal is to have one place to “showcase” all the Bus Pirate stuff and have relevant links to local distributors as well.

Another goal is to use the latest “special lines” that ship fast and dump off with the local post service (think aliexpress) with the VAT prepaid where applicable (EU, UK, MX). DirtyPCBs supports one VAT special line to germany, and we have to bill VAT manually.

I’ve chosen MedusaJS because it seems the most popular so problems I have will probably be documented somewhere.

The first step is a Python script to interact with MedusaJS because the admin backend is super tedious.

Medusa has a concept of regions that don’t really work for us because we ship world wide. It would create a separate region for each VAT rate and shipping method, plus a “world wide” category. It all gets a bit tedious, so let’s customize.

        headers = {
            "Accept": "application/json; charset=utf-8",
            "X-API-KEY": public_key
        }

        url_vat = url+"?limit=100"
        response = requests.get(url_vat, headers=headers)
        self.http_error(response)
        # loop, print country name, country code, standard rate
        rates = response.json().get('rates', [])
        for rate in rates:
            print(f"{{ country_name: \"{rate['country_name']}\", country_code: \"{rate['country_code']}\", rate: {rate['standard_rate']/100} }},")

First let’s pull a list of EU VAT rates from vatstack api using a bit of python to print a typescript object (yuck I know, but gotta start somewhere).

import {
  ITaxCalculationStrategy,
  LineItem,
  LineItemTaxLine,
  ShippingMethodTaxLine,
  TaxCalculationContext,
} from "@medusajs/medusa";

class TaxCalculationStrategy implements ITaxCalculationStrategy {
  async calculate(
    items: LineItem[],
    taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
    calculationContext: TaxCalculationContext
  ): Promise<number> {
    const { shipping_address } = calculationContext;
    const { shipping_methods } = calculationContext;

    console.log("tax strategy override");
    console.log(shipping_address.country_code);

    const prepaid_taxes: { country_name: string; country_code: string; rate: number; }[] = [
        { country_name: "Austria", country_code: "AT", rate: 0.2 },
        { country_name: "Belgium", country_code: "BE", rate: 0.21 },
        { country_name: "Bulgaria", country_code: "BG", rate: 0.2 },
        { country_name: "Croatia", country_code: "HR", rate: 0.25 },
        { country_name: "Cyprus", country_code: "CY", rate: 0.19 },
        { country_name: "Czech Republic", country_code: "CZ", rate: 0.21 },
        { country_name: "Denmark", country_code: "DK", rate: 0.25 },
        { country_name: "Estonia", country_code: "EE", rate: 0.22 },
        { country_name: "Finland", country_code: "FI", rate: 0.255 },
        { country_name: "France", country_code: "FR", rate: 0.2 },
        { country_name: "Germany", country_code: "DE", rate: 0.19 },
        { country_name: "Greece", country_code: "GR", rate: 0.24 },
        { country_name: "Hungary", country_code: "HU", rate: 0.27 },
        { country_name: "Ireland", country_code: "IE", rate: 0.23 },
        { country_name: "Italy", country_code: "IT", rate: 0.22 },
        { country_name: "Latvia", country_code: "LV", rate: 0.21 },
        { country_name: "Lithuania", country_code: "LT", rate: 0.21 },
        { country_name: "Luxembourg", country_code: "LU", rate: 0.17 },
        { country_name: "Malta", country_code: "MT", rate: 0.18 },
        { country_name: "Netherlands", country_code: "NL", rate: 0.21 },
        { country_name: "Poland", country_code: "PL", rate: 0.23 },
        { country_name: "Portugal", country_code: "PT", rate: 0.23 },
        { country_name: "Romania", country_code: "RO", rate: 0.19 },
        { country_name: "Slovakia", country_code: "SK", rate: 0.2 },
        { country_name: "Slovenia", country_code: "SI", rate: 0.22 },
        { country_name: "Spain", country_code: "ES", rate: 0.21 },
        { country_name: "Sweden", country_code: "SE", rate: 0.25 },
    ];    

    const rate = prepaid_taxes.find((tax) => tax.country_code.toUpperCase() === shipping_address.country_code.toUpperCase())?.rate || 0;

    console.log(rate);

    if(rate==0) return 0;

    var cart_total = 0;

    for (const item of items) {
        cart_total += (item.unit_price*item.quantity);
    }

    for (const shipping_method of shipping_methods) {
        cart_total += shipping_method.price;
    }
    return Math.round(cart_total * rate); 

  }
}

export default TaxCalculationStrategy;

tax-calculation.ts contains our “tax calculation strategy”. This overrides the default per-region tax setting. The important bit is logistics only allows prepaid VAT for certain countries. If a country is on the list, we get the VAT rate, total up the cost, and return the tax amount. Now we’re handing VAT independent of regionalized shops.

Next up, shipping API integration.

2 Likes

Is the shipping API integration only in regards to VAT rates, distributors, etc.? I wandered into this post to learn about the VAT in order to inform some colleagues. Now I am interested in the API, but wasn’t sure if this was internal only, as in authorized dealers, etc. Or, if there was some additional API info that I may have some interest in.

import {
    AbstractFulfillmentService,
    Cart,
    Fulfillment,
    LineItem,
    Order,
  } from "@medusajs/medusa";

class xyzFulfillmentService extends AbstractFulfillmentService {
    static identifier = "xyz-fulfillment";

  // methods here...
    constructor(container){
        super(container);
        // you can access options here

        // you can also initialize a client that
        // communicates with a third-party service.
        //this.client = new Client(options)
    }  


    async getFulfillmentOptions(): Promise<any[]> {
        //const shippingOptionsData = await this.getShippingMethods();

        //return shippingOptionsData.shipping_methods;
        return [
            {
              id: "my-fulfillment",
            },
            {
              id: "my-fulfillment-dynamic",
            },
        ]; 
    }

    // This method is used in different places, including:
    // When the shipping options for a cart are retrieved during checkout. If a shipping option has their price_type set to calculated, this method is used to set the amount of the returned shipping option.
    // When a shipping method is created. If the shipping option associated with the method has their price_type set to calculated, this method is used to set the price attribute of the shipping method in the database.
    // When the cart's totals are calculated.
    async calculatePrice(
        optionData: Record<string, unknown>,
        data: Record<string, unknown>,
        cart: Cart
    ): Promise<number> {
        const { shipping_address } = cart;
        console.log("xyz-fulfillment calculate price");
        console.log(data);
        console.log(optionData);
        console.log(cart);
        console.log(shipping_address.country_code);

        console.log(shipping_address.country_code);
        if (shipping_address.country_code.toUpperCase() == "NL") {
            console.log("NL");
            return 500; 
        }
        console.log("not NL");
        return null;
    }

    // Used to determine whether a shipping option is calculated dynamically or flat rate.
    async canCalculate(data: { [x: string]: unknown }): Promise<boolean> {
        return true;
    }    
          
    async validateFulfillmentData(
        optionData: { [x: string]: unknown; }, 
        data: { [x: string]: unknown; }, 
        cart: Cart
    ): Promise<Record<string, unknown>> {
        const { shipping_address } = cart;
        console.log("xyz-fulfillment validateFulfillmentData");
        console.log(optionData);
        console.log(data);
        console.log(cart);
        console.log(shipping_address.country_code);

        console.log(shipping_address.country_code);
        if (shipping_address.country_code.toUpperCase() == "NL") {
            console.log("NL");
            return {
                ...data
            };
        }
        console.log("not NL");
        throw new Error("not NL");
    }

    async validateOption(data: { [x: string]: unknown; }): Promise<boolean> {
        return true;
    }
    async createFulfillment(data: { [x: string]: unknown; }, items: LineItem[], order: Order, fulfillment: Fulfillment): Promise<{ [x: string]: unknown; }> {
        throw new Error("Method not implemented.");
    }
    async cancelFulfillment(fulfillment: { [x: string]: unknown; }): Promise<any> {
        throw new Error("Method not implemented.");
    }
    async createReturn(returnOrder): Promise<Record<string, unknown>> {
        throw new Error("Method not implemented.");
    }
    async getFulfillmentDocuments(data: { [x: string]: unknown; }): Promise<any> {
        throw new Error("Method not implemented.");
    }
    async getReturnDocuments(data: Record<string, unknown>): Promise<any> {
        throw new Error("Method not implemented.");
    }
    async getShipmentDocuments(data: Record<string, unknown>): Promise<any> {
        throw new Error("Method not implemented.");
    }
    async retrieveDocuments(fulfillmentData: Record<string, unknown>, documentType: "invoice" | "label"): Promise<any> {
        throw new Error("Method not implemented.");
    }
}

export default pfcFulfillmentService;

A bare bones fulfillment service provider function that feeds shipping options from our logistics provider’s API to the store.

image

Medusa has this awful concept of regions. All shipping and tax methods are attached to a region, and each region has a different storefront (think the little flags at digikey or mouser). When you go to checkout, you only see the countries serviced by that region.

We ship world wide, and the shipping method depends on location. It would be awfully confusing for users to have to choose from 150 some odd flags and be redirected to a local storefront. We want shipment methods to be determined by the ship address selection during checkout, like normal humans.

        if (shipping_address.country_code.toUpperCase() == "NL") {
            console.log("NL");
            return 500; 
        }
        console.log("not NL");
        return null;

During calculate price function we use a simple hack to return a price if the destination in NL, or null if it is not NL. This Sendcloud service does exactly this when the weight is too high for a specific shipping service, so I assumed it worked.

But it does not appear to work. Country is set to GB, the price returned is null, but then we see the NL option during checkout with $0 (choosing it results in validation error though).

    /**
     * Finds all the shipping profiles that cover the products in a cart, and
     * validates all options that are available for the cart.
     * @param cart - the cart object to find shipping options for
     * @return a list of the available shipping options
     */
    fetchCartOptions(cart: any): Promise<ShippingOption[]>;

It looks like this is the function that gathers the shipping methods. It can be overridden with a new shipping-profile.ts in the project services directory. However, for the life of me I can’t figure out where the function actually lives. This looks like a header file to me. Is that all the code? What on earth does it do? This is where I got stuck last night.

import { Lifetime } from "awilix"
import { 
    ShippingProfileService as MedusaShippingProfileService,
} from "@medusajs/medusa"

class ShippingProfileService extends MedusaShippingProfileService {
    // use the original fetchCartOptions method, but filter out methods where price is 0 or null
    async fetchCartOptions(cart: any){
        // filter out methods where price is 0 or null
        const shippingOptions = await super.fetchCartOptions(cart)
        return shippingOptions.filter((option) => option.amount > 0)
    }
    
  // ...
}

export default ShippingProfileService

Well, it turns out I don’t need to know anything about the underlying code to override it and add a filter for shipping methods with null price. Just extend the mystery class, override the fetchCartOptions function, but instead of reworking it, just call the base function and filter the result before returning. Easy!

Now we only have the shipping methods that don’t have a null cost.

There is still a ton of work to do, but this was the most “unknown” part of the project. It’s just pushing and pulling data from here on out.

1 Like

image

I hate working in big web frameworks :frowning: lost all morning because I didn’t realize all that JSON was inside that ‘d’ key.

image

At last, we have a list of available shipping methods in the back end.

2 Likes

image

Oh man. Everything is massively parallel/async, I have no idea how to cache stuff in this situation. Each trip over the firewall is really long, and we have to do it for each shipping channel even though the logistics API gives us all available channels and costs for a given country and weight.

I could:

  1. Let it be slow
  2. Figure out the proper medusajs way to do it (if you have years)
  3. Write a small python service that offloads it all to an environment that is easier to test and comprehend

Yes, I’m going with the worst options possible, #3.

3 Likes

I really like the PostFake Standard vs PostFake Express :rofl:
Never delivered vs also never delivered

I think I will wait for the shop to be ready and order some planks. For sure canbus plank and rs232 plank. I am also thinking about that flash chip planks but well I like the micro-clamps approach alot and of something I am waiting for the original Pomona Clip.

4 Likes
 async fetchCartOptions(cart: any){
        const { shipping_address } = cart;
        //console.log(cart);
        if(!shipping_address) return [];

        // filter out methods where price is 0 or null
        const selector = { is_return: false };
        const config = {}; // the configuration used to find the objects
        var shippingOptions = await this.shippingOptionService_.list(selector, config);
        
        //console.log(cart);
        let totalWeightGrams = 0;
        cart.items.forEach((item) => {
          const itemWeight = item.variant.product.weight;
          const itemQuantity = item.quantity;
    
          totalWeightGrams += itemWeight * itemQuantity;
        });
        const totalWeightKg = totalWeightGrams / 1000;
        console.log(totalWeightGrams);
        console.log(totalWeightKg);
        // query the pfc api for list of shipping options once
        // todo: country and weight from cart object
        let method: Method = 'POST';
        const shippingMethodsOptions = {
            method: method,
            url: '',
            headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            },
            data:{
                customerid:'',
                secretkey:'',
                country:shipping_address.country_code.toUpperCase(),
                weight:totalWeightKg,
                volume:'.1',
            }
        };
        const shippingOptionsResponse = await axios.request(shippingMethodsOptions);  
        //console.log(shippingOptionsResponse.data);
        const shippingOptionsData = shippingOptionsResponse.data;
        var pfcShippingOptions = eval(shippingOptionsData.d);

        // loop through the shipping options, assign the price from the pfc api if available
        for (const shippingOption of shippingOptions) {
            shippingOption.amount = null;
            for (const pfcShippingOption of pfcShippingOptions) {
                // if fulfillment provider and channel code match, assign the price
                if((shippingOption.provider_id=="pfc-fulfillment")&& (shippingOption.data.ChannelCode == pfcShippingOption.ChannelCode) ){
                    shippingOption.amount = Math.round((pfcShippingOption.MinSaleAmountTotal/6.2)*100);
                    break;
                }
            }
        }
            
        // return the shipping options
        //const shippingOptions = await super.fetchCartOptions(cart);
        //console.log(shippingOptions);
        //return shippingOptions;
        return shippingOptions.filter((option) => option.amount > 0);
    }

Instead of a whole production to create a cache to handle a bunch of async calls to the shipping API (in china, which is slow), I hacked the way it fetches shipping options to remove the async calls completely.

Expanding on the small hack to fetchCartOptions:

  • Grab all enabled shipping options
  • Grab the available shipping options from the logistics API per country and weight
  • Loop through the enabled shipping options and assign a price if available from the API

Now we can loop through a few dozen logistics options with a single call to the API in China.

Later we can cache this similar to how I cache results on Dirty PCBs.

Made the phone number and province/state required fields because all China logistics require a phone number these days.

Next up:

  • Send order and signup emails
  • Add payment processors
  • Finish logistics: create order, get labels, get status/tracking, etc.

Tax is now charged (or not) based on the shipping method. Some shipping methods don’t support prepaid VAT, such as normal post, the buyer has to pay when it arrives. Now we can support both types of shipment methods for VAT payment destinations (eg EU).

Living on the edge, so I stuck the storefront in a public github repo :slight_smile:

I don’t have the time to learn Next.js and tweak it the way I want, but I’ve hired someone to:

  • Home page carousel + Distributors images w/link (hand coded) + products list or categories
  • Format About page
  • Product photo aspect ratio
  • Password reset request, plus new password dialog (to be pull requested to the original project)
  • Remove country middleware?
  • Distributors on each product page

Early this week we’ll get this deployed, possibly on Vercel, but that seems overkill.

2 Likes

I know your still working hard on the site.

I just wanted to make a request for darkmode if not already added as an option. Also, i wanted to provide a headsup that your Mastodon link currently takes you to the forum and Bluesky link takes you to Mastodon:

Currently:
Mastodon > https://forum.buspirate.com/
Bluesky: > Ian at Dangerousprototypes (@buspirate@mastodon.social) - Mastodon

Keep up the good work, Im excited to see it coming along! Can’t wait to share it once its ready.

1 Like

Thanks! Looks like I missed a line :slight_smile:

1 Like

Hi everyone! I owe folks a bunch of replies, messages and bug fixes. The shop turned into a full time project this week. I’m continuing through the weekend and hope to have things back to normal in a few days.

Some of the things I’m excited about, and why this has eaten up a week of development time…

The shop is intended to be a single “showcase” for all the Bus Pirate stuff. There’s not a great way to see that at Dirty PCBs because it’s all under my “designer” account. It’s time for something more professional.

It also solves a bunch of requests we got when “launching” Bus Pirate 5 at the beginning of the year: guest checkout, not using paypal, enable all “special lines” with a new logistics company, and VAT pre-payment for places that matters (eg EU).

image

Bus Pirate 5 has a rapidly expanding distributor network. Distributors were super key to the 15 year success of the Bus Pirate, and I want to highlight them in the store. Each product detail page has a section with distributor logos and links to that product in the distributor’s shop.

Distributors are also highlighted on the home page below the announcement carousel.

3 Likes

The store appears to be ready, @jin has a better banner on the way.

It’s in playground mode, no payment method required to place an order. Feel free to check it out and place orders using the “manual” (test) payment method. It’s nice to have some data to try with the backend. All the orders will be erased when we move to production mode.

A 7 day holiday starts Monday, so we’ll wait until that is over to start taking orders.

Here’s the list of shipping methods available to the US :slight_smile: We’re using a new logistics company that provides:

  • “Special lines” are chartered freight flights with delivery by a local post office or courier. There are options for EU, US, Canada, Australia, UK, Brazil, several countries in the Middle East, and much of Asia.
  • VAT prepaid options for EU countries (local couriers/post services charge a 9-30Euro fee to asess VAT, so prepaying is cheaper). The logistics company does charge us a 2% fee for prepaid VAT, which we add to the freight costs.
  • All the typical express couriers (DHL, UPS, FEDEX). Most of these are directly operated, there shouldn’t be warehouse backlogs like we experience with the previous logistics company (yet to be seen, however).

MedusaJS (the store backend) doesn’t handle shipping in a way that works well with a Chinese logistics setup. We ship globally by many different methods, but some only go to one or two countries. Medusa expects the world to be more black and white with neatly defined regions containing countries serviced by a set of methods. Because of this I had to override a big chunk of the tax and fulfillment code in Medusa.

image

Even then, it hammers the logistics API (in China, across the firewall) so many times that selecting a shipping method can take minutes. I wrote a Python script to scrape shipping rates for all countries, all methods, in 100gram increments, and we cache that data locally on the Medusa server. It takes about 5 hours to run outside China. It should probably be moved to the Shenzhen office, with the scrape sent back when complete.

Internet traffic in and out of China can be really slow. DirtyPCBs’ fulfillment system makes about 8 trips across the firewall, and can be a real bottleneck to getting things shipped quickly.

image

To reduce the number of firewall crossings, I wrote a new fulfillment system that runs directly in a web browser in the Shenzhen office. Excuse the crude drawing.

  • The javascript app loads in a browser in Shenzhen
  • The app grabs orders from the shop backend periodically (one firewall crossing). This happens in the background, so if the connection is slow or fails it doesn’t effect the packing speed of the person using it.
  • The app (in China) creates shipments using the logistics API (also in China). This eliminates a whole slew of firewall crossings.
  • The app then periodically pushes shipped orders info back to the shop backend (one firewall hop). This is also a background operation.

This would be absolute insanity without IndexedDB in modern browsers. This is a little persistent storage database where we can save the incoming orders, save shipment data and track order status. The data should still be there if the tab is closed or the browser crashes.

image

I designed our own shipping label to apply the tracking barcode in the appropriate format (CODE128 in the example). Every logistics company we’ve worked with has a terrible API for retrieving shipping labels. They come in random shapes and sizes and formats. This isn’t the actual shipping label, it’s just the barcode they use internally, it will be replaced at the warehouse after the package is weighed. There will the one to four more labels applied depending on the shipping method (for example: logistics→freight→last mile delivery).

It turned into a monster project that involved multiple coding marathons, but it should suit our needs for a few years. I’m sorry I’ve been scarce for the last week or so. Thank you to everyone who helped out in the forum. I’ll be catching up this weekend.

REV2

This is a good start, but I have some thoughts for future improvements.

A lot of the stuff going on in the browser fulfillment app could be offloaded to a Python script running on a single board computer in the Shenzhen office. It’s a lot easier to consume data (e.g. randomly formatted label PDFs) and manipulate it from a Python script than browser javascript. I could also scrape shipping rates inside China.

While you can’t open port 80 on the public internet in China without an ICP license, we could run a local web server (Python/Flask) on the office intranet with data feeds for the fulfillment frontend. Using a single page website frame I’d make the fulfillment app more mobile friendly, and the team could then pick without printing packing lists.

Getting this all setup remotely would be a nightmare. It’s something to think about the next time I’m in the office though.

4 Likes

I enjoyed reading this. Thanks for putting all this together and integrating them into the back-end system. I’m excited to try this new system that does all the API calls in China. I can see the years of Chinese supply chain experience. :rofl:

3 Likes

I just played with the new shop. The focus on the BP products is much better than the dirtypcbs store for this. And the plethora of shipping options (I tried Germany because that is what I’m familiar with) is top notch - and USD 4.37 shipping cost is really cheap.

But as this is all quite new and hacked, I was looking at it with my senses alert for bugs and I poked it a bit with my software tester hat on. So I found some issues:

  • front page: it shows two pages of products with a “1” and “2”. Opening page 2 does a new browser request with a different url, but it shows the same set of products.

  • you wrote that you want to promote the distributors. you put the prominently on the front page. but when you open a product page, the distributors that have this product are hidden behind a panel on the left you have to open by clicking on the “+”. Having it open by default would promote the distributors much more than any front page listing in my opinion - because I guess the buyers look for the product first, and alternative buying options only when they have settled on a product they want.

  • I suggest to make the categories more prominent. They only show up on the bottom of the page. I would have expected them to be part of the menu that you can open at the top left.

  • checkout: I deliberately used a shipping address with the “company” field set, because I know it is a pain point in many shops. The company field I entered is not shown in the next step under shipping address - but the company field is an absolutely vital part of the shipping address because in the case of a company, the company name is often the only thing that the delivery guy will find written on the letter box. So make sure that it is shown as part of the shipping address as feedback to the user and doesn’t get lost in the process.

  • your software requires a “state” for Germany - that isn’t necessary, postal code + city name is enough.

  • your software requires a phone number. I know that in some countries, like for example russia, a phone number is absolutely vital to have your parcel delivered. But in other countries, like Germany, a phone number on the letter is useless and nobody will call it, even in case of problems during delivery.

  • I don’t understand how the VAT (in case of prepaid VAT) is calculated. German VAT is 19%, but it looks to me like you calculate with 20%. (BP 6+$4.37 shipping gave $17.34 tax)

3 Likes

Checked it out. electronic_eel provided a great set of bugs.

I wanted to highlight my experience, which I purposefully did prior to reading other folks feedback, so it might be closer to a first-experience a user might have. Below, I provide five bits of feedback that seem important to at least consider. I then list a few lower-priority items. All this feedback is free, and you get what you pay for, so feel free to consider it, throw it away or heckle me as appropriate. :wink:


1. (Previously noted) Anti-Dark-Mode Sadness

The blinding white photo backgrounds made the page something I didn’t want to keep using, as I use dark mode + DarkReader for Edge.

For comparison, an Adafruit picture:


2. Home page default product ordering

This relates to the home page.

The home page is your primary place to make a first impression.

Viewing with the eye of a first-time visitor, I do not feel showing the “newest products” is nearly as powerful as focusing on the first-time visitor. As a potential new user, I’d want to see the MAIN EVENT … the BusPirate5 hardware!

Recommended order of categories to be shown:
* BusPirate devices – MANUAL order for this listing (initially BP5, at least until BP6 gets stable)
* Cable Sets – This is the next thing most folks will want (upsell category)
* Planks – While these might not get picked up in the first order, having at least the category listed here will increase visibility of these awesome tools to make specialized tasks easier / expand the capabilities of the BP5 line.
* Individual Accessories – For folks that want to add to the cable sets, get more hooks, etc. Not a primary
* Upgrades

In addition, consider:

  • Use of collapsable category headers (e.g., accordian UI).
  • Expanding at least the first category (BP5 devices) by default
  • Expanding the first three categories (Devices, cable sets, planks) as it may improve sell-through, repeat visits (presume planks will be a later visit purchase), and visibility into the depth of capabilities the product enables
  • Note: For collapsed categories, can improve page load time by deferring the image loads for those categories (e.g., until the category is expanded, or more simply after remainder of page loads).
  • All the above are CDN-friendly options

User story for this feedback:
Billy, as a first-time-visitor, comes to the home page. As Billy doesn’t have any hardware / is new, it’s less likely she’ll care about what the newest release was. Rather, it’s more important to quickly and easily show off the four BP5 options (5, 5 2Gbit, 5XL, 6) as a group. The next thing Billy is likely to want are cables, so show the cable sets. While Billy doesn’t want to buy any planks at the moment, it plants the seed that planks can make certain tasks much easier. This increases the likelihood that Billy will look for a task-specific plank next time she visits.


3. Categories - Part 1

This relates to the page that lists all the products, aka “Store” link.

I never noticed the option to show the items by category in my first sample order. Moreover, even after reading about it’s existence in electronic_eel’s review, I still did not notice how to get to categorized items on my first two scans of the page. This strongly suggests that there’s an opportunity to improve the user experience here.

The current page that lists all the products shows “sort” options on the upper left. This is good.

I’d expect “categories” to be another option in that same area.

Steps that highlight my real-life, second-time visit, where I explicitly knew the categories option was “at the bottom of the page”:
* From home page, I clicked on menu, then “Store” to list all products. (In part, this was due to the lack of categorization on the home page … see above feedback item)
* I looked at the upper left, and notice the sort options by price, etc. – but no categories
* I scrolled to the bottom, and look under the last of the products … only to see the page 1 / page 2 options.
* I scrolled back to top, and scanned the whole list again, and still failed to notice any option to view by category
* A litte later, I finally realized the category filter is NOT part of the product list page, but rather exists only in the main footer of the site.


4. Categories - Part 2

When viewing all the products, there is no option to sort by category. This doesn’t “feel” right, and frankly made it hard to find the BusPirate hardware at all … never mind realizing there are different models or doing any comparison of them.

Similar to the home page feedback, I would like to suggest a checkbox (enabled by default) which sorts the results by category, preferably with collapsible category headers (e.g., accordian UI).

The recommended order is identical to the recommended order above for the home page (and intentionally so).
* BusPirate devices
* Cable Sets
* Planks
* Individual Accessories
* Upgrades


5. Categories - Part 3

This relates to the web footer links to specific item categories.

As noted above, I did eventually discover the category links in the footer of the page. I think it’s great to have these in the common footer for the site. First, my experience, and then a recommendation.

  1. Clicked on category for “Cables”
  2. Result was four items, all of which made sense.
  3. Expectation: I looked for a way to remove this filter
    a. Actual: There was no single-click way to explore all items
  4. Expectation: I looked for a way to go “up” in a tree (less applicable, but not uncommon metaphor)
    a. Actual: There was no tree-like hierarchy exposed in the UI, so again no single-click way to explore additional items.
  5. Frustrated, I finally clicked on the main menu, and then the “Store” option to see all items again … which emphasized again the lack of a categories .filter on the main product list page.

Recommendation:
As a user, my expectation when clicking that link would be to jump to the all-products listing, but with a filter pre-applied – “hiding” the items not in the selected category.

Critically, I would expect the category filter to be visible on that page, located proximate to the sort order, and modifiable … able to de-select or change which categories are being filtered.

Why this is good for the store:
Improves discoverability (and presumably sell-through)


And some low-priority or wishlist feedback:


6. Distributors - Part 1 (icons)

THIS IS A WISHLIST (low-priority) ITEM

As you noted, distributors have been key to the 15 year success of the Bus Pirate, and you’re wanting to highlight them in the store.

When looking at the list of all products (or a categorized list of products), the distributors that carry those items is not available.

Even after clicking into each individual product, the distributors are hidden by default:

Wishlist:
If distributors provide small (icon, 32x32?) square graphic, have at least an option to show the distributors for each product in the main product list. (e.g., a little row of icons)

Note: I realize this requires distributors to approve the logo used for the small square graphic. Most would likely have such items already created, but still time to ask for, get agreements for, and get the assets.


7. Distributors - Part 2 (filter)

THIS IS A WISHLIST (low-priority) ITEM.

Currently, the all products page allows sorting by price, newest, etc. Other feedback is asking to add the ability to filter by category of the item.

Please also consider allowing to filter based on distributor. E.g., If I already order things on a regular basis from Adafruit or HackerWarehouse, the current site design does not provide any easy way to figure out what I might add to my cart from those distributors.

Benefit to user: Finds items that they can get more quickly

Benefit to distributor: Leads!

Benefit to you: Goodwill with distributors, increased visibility of which items are carried may increase percentage of items carried by distributors


8. Distributors - Part 3 (cart)

THIS IS A WISHLIST (low-priority) ITEM, and in addition would be unnecessary if supporting either of the earlier Distributor feedback items.

As you noted, distributors have been key to the 15 year success of the Bus Pirate, and you’re wanting to highlight them in the store.

But I think few folks order only one item, especially when first moving to the BP5 platform. This store is the only authoritative site listing everything. As a starting user, let’s say I want to order a BP5, the two main cable sets, and the SIP adapter planks.

While I could order direct, shipping from a distributor would be faster for me. What I’d like to see here is an easy way to determine which (if any) of the distributors normally carry all the items in my cart.

This is currently REALLY slow and difficult for the user.

Benefit for the user: Faster shipping, likely cheaper shipping, etc.

Benefit for the distributor: More sales leads and goodwill

Benefit for you: Distributors may carry higher percentage of the available items (to match more folks’ carts and thus be listed as potential alternate purchase point for the items in the cart).


9. Cart Quick-Add

WISHLIST ITEM

It would be great to be able to, from the list of products, click to add an item to the cart, without loading each item’s individual pages.

E.g., an overlay button of “add to cart”, which does not leave the current page, but just adds the item to the cart.

Faster and more responsive for the user, vs. loading lots of pages.


All the above are (can be) CDN-friendly. Where the product list is a static download (and, for example, have a 4 hour cache expiration), and the filters are applied on the client side (e.g., JQuery + JQuery UI), performance should also improve for users.

4 Likes

I also noticed some of the points you explained, like the categories browsing and homepage points. But I was too lazy to explain them in such detail as you did. Thanks for doing that.

But after re-reading ians posts, I get the impression that he didn’t properly work on these sections at all for now, but mostly focused on getting the logistics and shipping cost calculation integration working. And I have to admit that this makes sense, because a nice front page is worth nothing if you can’t select a reasonable shipping option later for your order.

3 Likes

Thank you both SO much for checking out the shop and giving detailed feedback. I really appreciate it.

This is correct. I focused on the backend because nobody (I can afford…) can really replace me when it comes to the experience I’ve had with cross border logistics from Shenzhen and building systems to deal with it all.

The tax bug is defo my domain, thank you for checking. My guess is a silly rounding error somewhere at the lowest level of the tax calculator because the rate used to compute it seems to be correct (and is scraped periodically from here).

The storefront is Next.js, which is not something I care to learn a lot about even if I had the time :slight_smile: Fortunately it is possible to hire competent people (I can afford…) to make those kind of changes to the storefront. I have gathered all your valuable feeback and bug reports, and I will pass them on to the developer helping with the storefront.

I noticed in mobile mode that the cart link doesn’t pop up or do anything, a really nasty bug.

2 Likes

Thank you again for spotting the VAT issue. I’m attempting to debug it.

Backend debug output for changing shipping method

shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 445
Cart Total after shipping: 882
Total Tax: 168
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 445
Cart Total after shipping: 882
Total Tax: 168
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 0
Cart Total after shipping: 437
Total Tax: 83
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 445
Cart Total after shipping: 882
Total Tax: 168
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 445
Cart Total after shipping: 882
Total Tax: 168
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 0
Cart Total after shipping: 437
Total Tax: 83
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 445
Cart Total after shipping: 882
Total Tax: 168
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 445
Cart Total after shipping: 882
Total Tax: 168
shipping option tax prepaid
Tax Rate: 0.19
Cart Total after items: 0
Cart Total after shipping: 437
Total Tax: 83

A single cart update hits the tax module, what, 12 times? What a mess. It is highly parallel though…

Tax rate is correct for DE. The calculated values are correct. In the storefront the tax value is incorrect: 2.51.

2.51-1.68=0.83

There’s a couple hits to the tax module that don’t seem to contain items, just shipping method. It appears to be adding these two taxes together.

I don’t see anything in the docs that suggest this behavior. The tutorial/example is pretty straight forward.

Treat each call as having any possible combo of items and shipping

shipping option tax prepaid
Tax Rate: 0.19
Items Cost Total: 445
Items Tax: 84.55
Shipping Cost Total: 0
Shipping Methods Tax: 0
Cart Cost Total: 445
Cart Tax Total: 85
shipping option tax prepaid
Tax Rate: 0.19
Items Cost Total: 445
Items Tax: 84.55
Shipping Cost Total: 0
Shipping Methods Tax: 0
Cart Cost Total: 445
Cart Tax Total: 85
shipping option tax prepaid
Tax Rate: 0.19
Items Cost Total: 695
Items Tax: 132.05
Shipping Cost Total: 0
Shipping Methods Tax: 0
Cart Cost Total: 695
Cart Tax Total: 132
shipping option tax prepaid
Tax Rate: 0.19
Items Cost Total: 695
Items Tax: 132.05
Shipping Cost Total: 0
Shipping Methods Tax: 0
Cart Cost Total: 695
Cart Tax Total: 132
shipping option tax prepaid
Tax Rate: 0.19
Items Cost Total: 0
Items Tax: 0
Shipping Cost Total: 437
Shipping Methods Tax: 83.03
Cart Cost Total: 437
Cart Tax Total: 83

It seems that Medusa is making one tax call per line item, while I was treating it as calculating the cart in one go. Instead, I need to grab the lineItemsTaxLines, search through the cart data to find it, then tax those items. Repeat for shipping.

Items: [
        {
          id: 'item_01J9NW8K1AQC8H6E5N7FKYP1JX', //comes from lineItemsTaxLines
          quantity: undefined, // comes from items, match with id
          unit_price: undefined //comes from items, match with id
        }
      ]

It seems to be setup so the layer above can calculate one line at a time, or all lines in one go, depending on the request(s) in lineItemsTaxLines.

        const temp_items = lineItemsTaxLines.map((line) => {
            const item = items.find((i) => i.id === line.item_id);
            return {
                id: line.item_id,
                quantity: item?.quantity,
                unit_price: item?.unit_price
            };
        });

There also seems to be a Typescript magic way to access the item data through lineItemsTaxLines to avoid this kind of search. I don’t really care to invest the time to figure out how, we have a powerful server for medusa and low traffic.

I know from previous errors that I can’t pass decimals, so there’s going to be a per-line (or call?) rounding error in the total.

image

image

This looks correct now. Thank you so much for the bug report!

1 Like

yes, I just checked it again and now the VAT calculation results look correct to me.

Thanks for looking into this.

1 Like