fork download
  1. /*
  2.  * Expense Policy Rule Engine
  3.  *
  4.  * Problem asked in a Rippling interview.
  5.  *
  6.  * You get a list of expenses belonging to trips. Evaluate them against
  7.  * company policies and flag violations with reasons.
  8.  *
  9.  * Rules exist at two levels:
  10.  * - Expense-level (per expense)
  11.  * - Trip-level (across expenses of a trip)
  12.  *
  13.  * Design approach (matching the folder structure):
  14.  * - model/ → Expense, ExpenseType, Trip, Violation
  15.  * - rules/ → ExpenseRule, TripRule interfaces
  16.  * - rules/impl/ → DisallowRule, MaxAmountRule, TripTotalMaxRule, TripDurationRule
  17.  * - registry/ → RuleRegistry
  18.  * - engine/ → RuleEngine
  19.  * - service/ → ExpensePolicyService (facade)
  20.  * - util/ → TripValidator
  21.  *
  22.  * New rules can be added by implementing the right interface and registering them.
  23.  * No existing code needs to change.
  24.  *
  25.  * Note: Everything is in one file so it runs on Ideone.
  26.  * In a real project this would follow the folder structure shown in the diagram.
  27.  */
  28.  
  29. import java.util.*;
  30. import java.util.stream.Collectors;
  31.  
  32. enum ExpenseType {
  33. RESTAURANT, AIRFARE, ENTERTAINMENT, HOTEL, SUPPLIES
  34. }
  35.  
  36. // ==================== model/ ====================
  37. // Represents a single expense submitted by an employee
  38. class Expense {
  39. private final String expenseId;
  40. private final String tripId;
  41. private final double amountUSD;
  42. private final ExpenseType type;
  43.  
  44. public Expense(String expenseId, String tripId, double amountUSD, ExpenseType type) {
  45. this.expenseId = expenseId;
  46. this.tripId = tripId;
  47. this.amountUSD = amountUSD;
  48. this.type = type;
  49. }
  50.  
  51. public String getExpenseId() { return expenseId; }
  52. public String getTripId() { return tripId; }
  53. public double getAmountUSD() { return amountUSD; }
  54. public ExpenseType getType() { return type; }
  55. }
  56.  
  57. // Represents a trip that groups multiple expenses
  58. class Trip {
  59. private final String tripId;
  60. private final String name;
  61. private final int durationDays;
  62.  
  63. public Trip(String tripId, String name, int durationDays) {
  64. this.tripId = tripId;
  65. this.name = name;
  66. this.durationDays = durationDays;
  67. }
  68.  
  69. public String getTripId() { return tripId; }
  70. public String getName() { return name; }
  71. public int getDurationDays() { return durationDays; }
  72. }
  73.  
  74. // Holds details of a rule violation
  75. class Violation {
  76. private final String message;
  77. private final String expenseId; // null for trip-level violations
  78.  
  79. private Violation(String message, String expenseId) {
  80. this.message = message;
  81. this.expenseId = expenseId;
  82. }
  83.  
  84. public static Violation of(String message) {
  85. return new Violation(message, null);
  86. }
  87.  
  88. public static Violation of(String message, String expenseId) {
  89. return new Violation(message, expenseId);
  90. }
  91.  
  92. public String getMessage() { return message; }
  93. public String getExpenseId() { return expenseId; }
  94.  
  95. @Override
  96. public String toString() {
  97. return expenseId != null ? "[" + expenseId + "] " + message : message;
  98. }
  99. }
  100.  
  101. // ==================== rules/ ====================
  102. // Contract for rules that check a single expense
  103. interface ExpenseRule {
  104. Optional<Violation> check(Expense expense);
  105. }
  106.  
  107. // Contract for rules that check an entire trip
  108. interface TripRule {
  109. Optional<Violation> check(Trip trip, List<Expense> expenses);
  110. }
  111.  
  112. // ==================== rules/impl/ ====================
  113. // Prevents a specific expense type (e.g. no airfare allowed)
  114. class DisallowRule implements ExpenseRule {
  115. private final ExpenseType disallowedType;
  116.  
  117. public DisallowRule(ExpenseType disallowedType) {
  118. this.disallowedType = disallowedType;
  119. }
  120.  
  121. @Override
  122. public Optional<Violation> check(Expense expense) {
  123. if (expense.getType() == disallowedType) {
  124. return Optional.of(Violation.of(disallowedType + " expenses are not allowed", expense.getExpenseId()));
  125. }
  126. return Optional.empty();
  127. }
  128. }
  129.  
  130. // Checks if a single expense exceeds a maximum amount
  131. class MaxAmountRule implements ExpenseRule {
  132. private final double maxAmount;
  133.  
  134. public MaxAmountRule(double maxAmount) {
  135. this.maxAmount = maxAmount;
  136. }
  137.  
  138. @Override
  139. public Optional<Violation> check(Expense expense) {
  140. if (expense.getAmountUSD() > maxAmount) {
  141. return Optional.of(Violation.of(
  142. "Amount $" + expense.getAmountUSD() + " exceeds max $" + maxAmount,
  143. expense.getExpenseId()));
  144. }
  145. return Optional.empty();
  146. }
  147. }
  148.  
  149. // Checks if the total spend of a trip exceeds a limit
  150. class TripTotalMaxRule implements TripRule {
  151. private final double maxAmount;
  152.  
  153. public TripTotalMaxRule(double maxAmount) {
  154. this.maxAmount = maxAmount;
  155. }
  156.  
  157. @Override
  158. public Optional<Violation> check(Trip trip, List<Expense> expenses) {
  159. double total = expenses.stream().mapToDouble(Expense::getAmountUSD).sum();
  160. if (total > maxAmount) {
  161. return Optional.of(Violation.of("Trip total $" + total + " exceeds max $" + maxAmount));
  162. }
  163. return Optional.empty();
  164. }
  165. }
  166.  
  167. // Checks if the trip duration exceeds a limit
  168. class TripDurationRule implements TripRule {
  169. private final int maxDays;
  170.  
  171. public TripDurationRule(int maxDays) {
  172. this.maxDays = maxDays;
  173. }
  174.  
  175. @Override
  176. public Optional<Violation> check(Trip trip, List<Expense> expenses) {
  177. if (trip.getDurationDays() > maxDays) {
  178. return Optional.of(Violation.of("Trip duration " + trip.getDurationDays() + " days exceeds max " + maxDays));
  179. }
  180. return Optional.empty();
  181. }
  182. }
  183.  
  184. // ==================== util/ ====================
  185. // Optional helper to validate that all expenses belong to the same trip
  186. class TripValidator {
  187. public static boolean allExpensesBelongToSameTrip(List<Expense> expenses) {
  188. if (expenses == null || expenses.isEmpty()) return true;
  189. String tripId = expenses.get(0).getTripId();
  190. return expenses.stream().allMatch(e -> e.getTripId().equals(tripId));
  191. }
  192. }
  193.  
  194. // ==================== registry/ ====================
  195. // Maintains which rules apply at expense and trip levels
  196. class RuleRegistry {
  197. private final Map<ExpenseType, List<ExpenseRule>> typeRules = new HashMap<>();
  198. private final List<ExpenseRule> globalExpenseRules = new ArrayList<>();
  199. private final List<TripRule> tripRules = new ArrayList<>();
  200.  
  201. public void addTypeRule(ExpenseType type, ExpenseRule rule) {
  202. typeRules.computeIfAbsent(type, k -> new ArrayList<>()).add(rule);
  203. }
  204.  
  205. public void addGlobalExpenseRule(ExpenseRule rule) {
  206. globalExpenseRules.add(rule);
  207. }
  208.  
  209. public void addTripRule(TripRule rule) {
  210. tripRules.add(rule);
  211. }
  212.  
  213. // Returns type-specific + global expense rules
  214. public List<ExpenseRule> getRulesFor(ExpenseType type) {
  215. List<ExpenseRule> rules = new ArrayList<>(globalExpenseRules);
  216. rules.addAll(typeRules.getOrDefault(type, Collections.emptyList()));
  217. return rules;
  218. }
  219.  
  220. public List<TripRule> getTripRules() {
  221. return tripRules;
  222. }
  223. }
  224.  
  225. // ==================== engine/ ====================
  226. interface RuleEngine {
  227. List<Violation> evaluate(Trip trip, List<Expense> expenses, RuleRegistry registry);
  228. }
  229.  
  230. class SimpleRuleEngine implements RuleEngine {
  231. @Override
  232. public List<Violation> evaluate(Trip trip, List<Expense> expenses, RuleRegistry registry) {
  233. List<Violation> violations = new ArrayList<>();
  234.  
  235. // Optional validation
  236. if (!TripValidator.allExpensesBelongToSameTrip(expenses)) {
  237. violations.add(Violation.of("Expenses belong to different trips"));
  238. return violations;
  239. }
  240.  
  241. // Apply all expense-level rules
  242. for (Expense expense : expenses) {
  243. for (ExpenseRule rule : registry.getRulesFor(expense.getType())) {
  244. rule.check(expense).ifPresent(violations::add);
  245. }
  246. }
  247.  
  248. // Apply all trip-level rules
  249. for (TripRule rule : registry.getTripRules()) {
  250. rule.check(trip, expenses).ifPresent(violations::add);
  251. }
  252.  
  253. return violations;
  254. }
  255. }
  256.  
  257. // ==================== service/ ====================
  258. // Facade / entry point that clients use to evaluate expenses
  259. class ExpensePolicyService {
  260. private final RuleRegistry registry;
  261. private final RuleEngine engine;
  262.  
  263. public ExpensePolicyService(RuleRegistry registry, RuleEngine engine) {
  264. this.registry = registry;
  265. this.engine = engine;
  266. }
  267.  
  268. public List<Violation> evaluate(Trip trip, List<Expense> expenses) {
  269. return engine.evaluate(trip, expenses, registry);
  270. }
  271. }
  272.  
  273. // ==================== Main (for Ideone) ====================
  274. public class Main {
  275. public static void main(String[] args) {
  276. // Sample trip and expenses
  277. Trip trip = new Trip("T1", "Client Visit", 5);
  278.  
  279. List<Expense> expenses = Arrays.asList(
  280. new Expense("E1", "T1", 80, ExpenseType.RESTAURANT),
  281. new Expense("E2", "T1", 120, ExpenseType.AIRFARE),
  282. new Expense("E3", "T1", 40, ExpenseType.ENTERTAINMENT),
  283. new Expense("E4", "T1", 200, ExpenseType.HOTEL)
  284. );
  285.  
  286. // Register rules
  287. RuleRegistry registry = new RuleRegistry();
  288. registry.addTypeRule(ExpenseType.RESTAURANT, new MaxAmountRule(75));
  289. registry.addTypeRule(ExpenseType.AIRFARE, new DisallowRule(ExpenseType.AIRFARE));
  290. registry.addTypeRule(ExpenseType.ENTERTAINMENT, new DisallowRule(ExpenseType.ENTERTAINMENT));
  291. registry.addGlobalExpenseRule(new MaxAmountRule(250));
  292. registry.addTripRule(new TripTotalMaxRule(300));
  293. registry.addTripRule(new TripDurationRule(7));
  294.  
  295. // Evaluate using the service
  296. ExpensePolicyService service = new ExpensePolicyService(registry, new SimpleRuleEngine());
  297. List<Violation> violations = service.evaluate(trip, expenses);
  298.  
  299. System.out.println("=== Violations ===");
  300. if (violations.isEmpty()) {
  301. System.out.println("No violations.");
  302. } else {
  303. violations.forEach(v -> System.out.println("- " + v));
  304. }
  305. }
  306. }
Success #stdin #stdout 0.18s 61220KB
stdin
Standard input is empty
stdout
=== Violations ===
- [E1] Amount $80.0 exceeds max $75.0
- [E2] AIRFARE expenses are not allowed
- [E3] ENTERTAINMENT expenses are not allowed
- Trip total $440.0 exceeds max $300.0