Start learning free

LESSON 561 OF 569 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in JavaScript: Abstract Class Convention

Practice O(1) Read the full explanation

A shop takes payments by card, by PayPal and in cash. Every payment has an amount, in cents, and a total: the amount plus a processing fee. The total is worked out the same way for every payment; only the fee depends on the kind of payment. That is what an abstract class is for: it holds the shared part, says that every payment has a fee, and leaves the fee itself to each kind of payment.

  • The abstract class Payment is built from an amount, in cents, and keeps it. Its method fee() is abstract: every payment has a fee, but Payment can't say what it is. Its method total() is concrete: it returns the amount plus fee().
  • Card, PayPal and Cash inherit from Payment, keep its total() as it is, and each writes its own fee():
  • Card pays 2%: its fee is amount × 2 / 100.
  • PayPal pays 3% plus 30 cents: its fee is amount × 3 / 100 + 30.
  • Cash costs nothing: its fee is 0.

JavaScript has no abstract classes. As in the abstract-classes lesson, Payment's own fee() throws new Error("Not implemented"), and each subclass (extends Payment) replaces it with its own. The subclasses inherit the constructor and total. / keeps the fraction, so round the fee down with Math.floor.

Every division rounds down: a card payment of 1999 cents has a fee of 39, not 39.98.

The starter code already has every class and every method header: write the bodies. For each test, the harness creates the payments in the order given and prints two lines: each payment's total, total(), separated by spaces, and then the sum of all the fees. It calls them through the abstract type Payment, never through Card, PayPal or Cash.

 

Example 1:

Input:
    new Card(1999)
    new PayPal(500)
    new Cash(700)
Output:
    2038 545 700
    84
Explanation: The card's fee is 1999 × 2 / 100 = 39 (39.98 rounded down), so its total is 2038. PayPal's fee is 500 × 3 / 100 + 30 = 45, total 545. Cash has no fee: total 700. The fees add up to 39 + 45 + 0 = 84.

Example 2:

Input:
    new Card(49)
    new PayPal(33)
    new Card(50)
Output:
    49 63 51
    31
Explanation: 49 × 2 / 100 is 0.98, rounded down to 0. PayPal's 33 × 3 / 100 is 0.99, rounded down to 0, plus 30. The second card's 50 × 2 / 100 is exactly 1. The fees are 0 + 30 + 1 = 31.

Example 3:

Input:
    new Card(0)
    new PayPal(0)
    new Cash(0)
Output:
    0 30 0
    30
Explanation: Amounts can be 0. PayPal still adds its 30 cents.

 

Constraints:

  • Every test has between 1 and 100 payments.
  • Every amount is an integer from 0 to 106 cents.

Note:

Every fee and every total takes O(1) time.

STEP 1 OF 6
    Ctrl + Enter to check

    JAVASCRIPT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in JavaScript, explained

    Problem overview

    A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents and a total, which is the amount plus a processing fee. The total is worked out the same way for every payment. Only the fee depends on the kind of payment, so the shared part goes in a parent class, Payment, and each kind writes only its fee.

    • Payment(amount) keeps the amount. Its fee() is abstract: every payment has a fee, but Payment cannot say what it is. Its total() is concrete and returns the amount plus fee().
    • Card: fee is amount * 2 / 100, rounded down.
    • PayPal: fee is amount * 3 / 100, rounded down, plus 30.
    • Cash: fee is 0.

    A card payment of 1999 cents has a fee of 39 (39.98 rounded down), so its total is 2038. A PayPal payment of 500 has a fee of 15 + 30 = 45 and a total of 545. A cash payment of 700 has a total of 700.

    Approach: one total, three fees

    Writing total() in each of the three classes would repeat the same line three times. Instead, total() lives once in Payment and calls this.fee(). When it runs for a card payment, this is a Card object, so this.fee() runs the card's fee. The parent method calls a method that only the subclasses define. That is the point of an abstract class.

    JavaScript has no abstract keyword. The convention is that the parent's fee() throws an error, and each subclass replaces it. A subclass that forgets to write its own fee() then fails loudly instead of quietly charging nothing.

    Step by step

    1. Store the amount. Payment's constructor saves it in this.amount. The subclasses have no constructors, so they inherit this one, and new Card(1999) passes 1999 up to it.
    2. Make fee() abstract. It throws new Error("Not implemented").
    3. Write total(). It returns this.amount + this.fee(), once for every kind of payment.
    4. Write Card.fee(). Multiply by 2, divide by 100, and round down with Math.floor.
    5. Write PayPal.fee(). Multiply by 3, divide by 100, round down, then add 30. The 30 cents are added even when the amount is 0.
    6. Write Cash.fee(). It returns 0. Without it, a cash payment would run Payment's fee() and throw.

    The solution in JavaScript

    class Payment {
        constructor(amount) {
            this.amount = amount;
        }
    
        fee() {
            throw new Error("Not implemented");
        }
    
        total() {
            return this.amount + this.fee();
        }
    }
    
    class Card extends Payment {
        fee() {
            return Math.floor(this.amount * 2 / 100);
        }
    }
    
    class PayPal extends Payment {
        fee() {
            return Math.floor(this.amount * 3 / 100) + 30;
        }
    }
    
    class Cash extends Payment {
        fee() {
            return 0;
        }
    }
    

    Rounding

    In JavaScript / keeps the fraction: 1999 * 2 / 100 is 39.98. Math.floor rounds it down to 39. Round once, after the multiplication and the division. Rounding in the middle changes the answer: for an amount of 150, Math.floor(150 / 100) * 2 is 2, while the correct card fee, Math.floor(150 * 2 / 100), is 3. The order amount * 2 / 100 multiplies first and divides second, so the fraction survives until the single Math.floor.

    For PayPal, round the percentage first and then add 30. The 30 cents are a whole number already.

    Using the classes

    The three payments can sit in one array, and the same two calls work on each:

    const payments = [new Card(1999), new PayPal(500), new Cash(700)];
    console.log(payments.map(p => p.total()).join(" "));
    console.log(payments.reduce((sum, p) => sum + p.fee(), 0));
    2038 545 700
    84

    The fees are 39 + 45 + 0 = 84. A second set shows the rounding. new Card(49) has a fee of 0 (0.98 rounded down), new PayPal(33) has 0 + 30, and new Card(50) has exactly 1:

    49 63 51
    31

    An amount of 0 is allowed. new Card(0), new PayPal(0) and new Cash(0) have totals 0, 30 and 0, and the fees add up to 30.

    Why not repeat total() in each class

    Suppose the shop later adds a small rounding rule to every total. If total() were copied into Card, PayPal and Cash, the change would have to be made three times, and one copy could easily be missed. With total() in Payment, there is one place to edit. The fee is the only part that really differs between the kinds, so it is the only part the subclasses write. A fourth kind of payment, a gift card for instance, needs just a new class with its own fee(), and it gets the constructor and total() for free.

    Because every call goes through the Payment methods, the code that adds up a list of payments never needs to ask which kind each one is. It calls total() or fee(), and each object answers for itself.

    Edge cases and common mistakes

    • Forgetting a subclass's fee(). A class such as class Check extends Payment {} inherits the parent's fee(), so new Check(100).total() throws Not implemented.
    • Calling Payment directly. new Payment(100).total() throws the same error: Payment has no fee of its own to add.
    • Leaving out this.. Inside total(), writing amount + fee() refers to variables that do not exist. Methods and fields are reached through this.
    • Skipping Math.floor. A card payment of 1999 would get a fee of 39.98 and a total of 2038.98.
    • Adding the 30 inside the division. Math.floor((amount * 3 + 30) / 100) is a different calculation. The 30 cents are added after the rounding.

    Key takeaways

    • Put the shared rule in the parent and let it call the method the subclasses define.
    • In JavaScript an abstract method is a method that throws, replaced by every subclass.
    • Round once, at the end of the percentage calculation, and add whole-cent amounts afterwards.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free