Code · Interview problems · Problems

Payment Invoice Matching

Medium String parsing / Sorting 3 parts 211 lines View on GitHub ↗

Question

Topic: String parsing / Sorting · Difficulty: Easy → Medium

A payment is paymentId,amount,memo where the memo may say Paying off: <invoiceId> or Paying for: <invoiceId>. Invoices are invoiceId,amount,dueDate.

Part 1: match by memo

Read the invoice id from the memo (case-insensitive marker) and return "<paymentId> pays off <amount> for <invoiceId> due on <dueDate>", or "<paymentId> could not be matched to any invoice".

Part 2: fall back to amount

If the memo doesn’t name a known invoice, match an invoice with the same amount. If several match, pick the earliest due date (then the smallest id).

Part 3: forgiveness

If nothing matches exactly, allow a short payment within forgiveness (amount < invoiceAmount ≤ amount + forgiveness) and append ", forgiving <forgiveness>".

Approach: parse once, then try the matchers in order: memo, exact amount, forgiveness. Break ties by sorting on (dueDate, id).

PaymentInvoiceMatchPart1.java 45 lines GitHub ↗
/**
 * Problem: Payment Invoice Matching
 * Difficulty: Medium
 * Topic: String parsing / Sorting
 * Question: Match a payment to an invoice by memo, then by equal amount (earliest due date), then within a forgiveness amount. Full statement in README.md.
 */
package dsa.question7;

import java.util.HashMap;

public class PaymentInvoiceMatchPart1 {

    public static class Inovice{
        String invoiceID;
        Long amount;
        String dueDate;
        public Inovice(String invoiceID, Long amount, String dueDate) {
            this.invoiceID = invoiceID ; this.amount = amount; this.dueDate = dueDate;
        }
    }

    public static String paymentMatch(String payment, String[] invoices) {
        HashMap<String, Inovice> invoiceDetail = new HashMap<>();

        for(int i=0; i< invoices.length; i++) {
            String[] invoice = invoices[i].split(",");
            String invoiceId = invoice[0] , dueDate = invoice[2];
            Long amount = Long.parseLong(invoice[1]);
            invoiceDetail.put(invoiceId, new Inovice(invoiceId, amount, dueDate));
        }

        String[] paymentDetail = payment.split("," , 3);
        String paymentId = paymentDetail[0];
        Long amount = Long.parseLong(paymentDetail[1]);
        String matchInvoice = paymentDetail[2];
        String lowered = matchInvoice.toLowerCase();
        String invoiceID = "";

        for(String marker : new String[] {"paying off:" , "paying for:"}) {
            int index = lowered.indexOf(marker);
            if(index != -1) {
                invoiceID = matchInvoice.substring(index + marker.length()).trim();
                break;
            }
        }

        if(!invoiceDetail.containsKey(invoiceID)) {
            return paymentId + " could not be matched to any invoice";
        }

        String duedate = invoiceDetail.get(invoiceID).dueDate;

        return paymentId + " pays off " + amount + " for " + invoiceID + " due on " + duedate;
    }

}
PaymentInvoiceMatchPart2.java 72 lines GitHub ↗
package dsa.question7;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

public class PaymentInvoiceMatchPart2 {

    public static class Inovice{
        String invoiceID;
        Long amount;
        String dueDate;
        public Inovice(String invoiceID, Long amount, String dueDate) {
            this.invoiceID = invoiceID ; this.amount = amount; this.dueDate = dueDate;
        }
    }

    public static class Payment {
        String paymentId;
        Long amount;
        String invoiceId;
        public Payment(String paymentId, Long amount , String invoiceId) {
            this.paymentId = paymentId ; this.amount = amount ; this.invoiceId = invoiceId;
        }
    }


    public static String paymentMatch(String payment, String[] invoices) {

        List<Inovice> invoiceDetail = new ArrayList<>();
        for(int i=0; i< invoices.length; i++) {
            String[] invoice = invoices[i].split(",");
            String invoiceId = invoice[0] , dueDate = invoice[2];
            Long amount = Long.parseLong(invoice[1]);
            invoiceDetail.add(new Inovice(invoiceId, amount, dueDate));
        }

        String[] paymentDetail = payment.split("," , 3);
        String paymentId = paymentDetail[0];
        Long amount = Long.parseLong(paymentDetail[1]);
        String matchInvoice = paymentDetail[2];
        String lowered = matchInvoice.toLowerCase();
        String invoiceID = "";

        for(String marker : new String[] {"paying off:" , "paying for:"}) {
            int index = lowered.indexOf(marker);
            if(index != -1) {
                invoiceID = matchInvoice.substring(index + marker.length()).trim();
                break;
            }
        }

        Payment paymentParse = new Payment(paymentId, amount, invoiceID);

        for(Inovice book : invoiceDetail) {
            if(book.invoiceID.equals(invoiceID)) {
                String duedate = book.dueDate;
                return paymentId + " pays off " + amount + " for " + invoiceID + " due on " + duedate;
            }
        }

        Inovice recociller = reconcileOnAmount(paymentParse, invoiceDetail);

        if(recociller != null) {
            return paymentId + " pays off " + amount + " for " + recociller.invoiceID + " due on " + recociller.dueDate;
        }


        return paymentId + " could not be matched to any invoice";
    }


    private static Inovice reconcileOnAmount(Payment payment, List<Inovice> invoiceDetail ) {

        List<Inovice> amountMatched = new ArrayList<>();
        for(Inovice book : invoiceDetail) {
            if(book.amount.equals(payment.amount)) {
                amountMatched.add(book);
            }
        }

        if(amountMatched.isEmpty()) {
            return null;
        }

        // sort on due date and return first
        amountMatched.sort(Comparator.comparing( (Inovice a) -> a.dueDate )
                .thenComparing(a -> a.invoiceID));

        return amountMatched.get(0);
    }
}
PaymentInvoiceMatchPart3.java 94 lines GitHub ↗
package dsa.question7;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class PaymentInvoiceMatchPart3 {

    public static class Inovice{
        String invoiceID;
        Long amount;
        String dueDate;
        public Inovice(String invoiceID, Long amount, String dueDate) {
            this.invoiceID = invoiceID ; this.amount = amount; this.dueDate = dueDate;
        }
    }

    public static class Payment {
        String paymentId;
        Long amount;
        String invoiceId;
        public Payment(String paymentId, Long amount , String invoiceId) {
            this.paymentId = paymentId ; this.amount = amount ; this.invoiceId = invoiceId;
        }
    }


    public static String paymentMatch(String payment, String[] invoices, Long forgiveness) {

        List<Inovice> invoiceDetail = new ArrayList<>();
        for(int i=0; i< invoices.length; i++) {
            String[] invoice = invoices[i].split(",");
            String invoiceId = invoice[0] , dueDate = invoice[2];
            Long amount = Long.parseLong(invoice[1]);
            invoiceDetail.add(new Inovice(invoiceId, amount, dueDate));
        }

        String[] paymentDetail = payment.split("," , 3);
        String paymentId = paymentDetail[0];
        Long amount = Long.parseLong(paymentDetail[1]);
        String matchInvoice = paymentDetail[2];
        String lowered = matchInvoice.toLowerCase();
        String invoiceID = "";

        for(String marker : new String[] {"paying off:" , "paying for:"}) {
            int index = lowered.indexOf(marker);
            if(index != -1) {
                invoiceID = matchInvoice.substring(index + marker.length()).trim();
                break;
            }
        }

        Payment paymentParse = new Payment(paymentId, amount, invoiceID);

        for(Inovice book : invoiceDetail) {
            if(book.invoiceID.equals(invoiceID)) {
                String duedate = book.dueDate;
                return paymentId + " pays off " + amount + " for " + invoiceID + " due on " + duedate;
            }
        }

        Inovice recocillerOnExactMatch = recocillerOnExactMatch(paymentParse, invoiceDetail);

        if(recocillerOnExactMatch != null) {
            return paymentId + " pays off " + amount + " for " + recocillerOnExactMatch.invoiceID + " due on " + recocillerOnExactMatch.dueDate;
        }

        Inovice reconcileOnForgiveness = reconcileOnForgiveness(paymentParse, invoiceDetail, forgiveness);

        if(reconcileOnForgiveness != null) {
            return paymentId + " pays off " + amount + " for " + reconcileOnForgiveness.invoiceID + " due on " + reconcileOnForgiveness.dueDate+ ", forgiving " + forgiveness;
        }


        return paymentId + " could not be matched to any invoice";
    }

    private static Inovice recocillerOnExactMatch(Payment payment, List<Inovice> invoiceDetail) {

        List<Inovice> amountMatched = new ArrayList<>();
        for(Inovice book : invoiceDetail) {
            Long amountDue = book.amount;
            Long amountPay = payment.amount;
            if(amountPay.equals(amountDue)) {
                amountMatched.add(book);
            }
        }

        if(amountMatched.isEmpty()) {
            return null;
        }

        // sort on due date and return first
        amountMatched.sort(Comparator.comparing( (Inovice a) -> a.dueDate )
                .thenComparing(a -> a.invoiceID));

        return amountMatched.get(0);
    }

    private static Inovice reconcileOnForgiveness(Payment payment, List<Inovice> invoiceDetail, Long forgiveness) {

        List<Inovice> amountMatched = new ArrayList<>();
        for(Inovice book : invoiceDetail) {
            Long amountDue = book.amount;
            Long amountPay = payment.amount;
            if(amountPay < amountDue  && amountDue <= amountPay + forgiveness) {
                amountMatched.add(book);
            }
        }

        if(amountMatched.isEmpty()) {
            return null;
        }

        // sort on due date and return first
        amountMatched.sort(Comparator.comparing( (Inovice a) -> a.dueDate )
                .thenComparing(a -> a.invoiceID));

        return amountMatched.get(0);
    }
}