SalesEngine
In this project you’ll practice building a system of several interacting Ruby objects using TDD.
This project is open source. If you notice errors, typos, or have questions/suggestions, please submit them to the project on Github.
Learning Goals
- Build a system using multiple interacting classes
- Use duck typing to share interactions across similar types
- Use modules to share common code
- Maintain high test coverage
- Use tests to drive creation of code
Abstract
Let’s write a data reporting tool that manipulates and reports on merchant transactional data.
Getting Started
- One team member forks the repository at https://github.com/gschool/sales_engine
- Add the second team member as a collaborator
Data Supplied
We have several files of source data including:
customers.csv- customer names and other attributestransactions.csv- individual transactions with a marker relating a customer, merchant, invoice, and credit cardinvoices.csv- invoices that link transactions to invoice items and hold a statusinvoice_items.csv- the item, quantity, and unit price paid for an item in a transactionitems.csv- items available for sale at the merchantsmerchants.csv- merchant names and identifying information
Dig into the data files to understand how everything is linked together.
https://github.com/gSchool/sales_engine/tree/master/data
Understandings
- The data was created from customer orders where:
- One invoice connects the customer to multiple invoice items, one or more transactions, and one merchant
- At least one transaction where their credit card is charged. If the charge fails, more transactions may be created for that single invoice.
- One or more invoice items: one for each item that they ordered
- The transaction references only the invoice
- The invoice item references an item and an invoice
- The item is connected to many invoice items and one merchant
- The merchant is connected to many invoices and many items
- Prices, as represented in the CSVs, are in cents. Anytime you return a revenue total (like in
Merchant#revenue) you must return aBigDecimalrepresentating dollars and cents (two decimals, rounded to the nearest cent).
Restrictions
Project implementation may not use:
- Rails’
ActiveRecordlibrary or a similar object-relational mappers (includingSequel,DataMapper, etc) - Your implementation may not use
StructorOpenStruct
Test Independence
In addition, each of your test files must be able to run independently. You’ll normally run your tests like this:
1
| |
But each of your test files must also work independently like this:
1 2 | |
Base Expectations
You are to build several classes implementing an API which allows for querying of this data including the objects and methods listed below. Note that . signifies a class method and # signifies an instance method.
Before digging too deeply into the listed methods below, you need to build a system which can parse the data files and create relationships between all the various objects.
Packaging
Your code must be wrapped up in a gem named "sales_engine". You should not push it up to Rubygems, but it should be installable/runnable from your Github repository.
In the evaluation process, we will be using this test suite to evalue your code:
Within the Sales Engine Test Suite is a Gemfile which loads your repository as a dependency based on the filepath location:
1 2 3 4 | |
The evaluator will run the following commands in the test suite project:
Terminal
$ $ | |
Entry Point
Functionality of your code will be assessed by an automated test suite. The evaluator of your code will:
- Clone your SalesEngine project from Github
- Run
bundleto install any dependencies - Call
Bundler.requireto load all dependencies - Call
SalesEngine.startupto execute any pre-loading or parsing - Run the methods defined below in any valid sequence
- Exit with a performance summary
Searching
For your Merchant, Invoice, Item, InvoiceItem, and Customer classes you need to build:
.randomreturns a random instance.find_by_X(match), whereXis some attribute, returns a single instance whoseXattribute case-insensitive attribute matches thematchparameter. For instance,Customer.find_by_first_name("Mary")could find aCustomerwith the first name attribute"Mary"or"mary"but not"Mary Ellen"..find_all_by_X(match)works just like.find_by_Xexcept it returns a collection of all matches. If there is no match, it returns an emptyArray.
Relationships
Merchant
#itemsreturns a collection ofIteminstances associated with that merchant for the products they sell#invoicesreturns a collection ofInvoiceinstances associated with that merchant from their known orders
Invoice
#transactionsreturns a collection of associatedTransactioninstances#invoice_itemsreturns a collection of associatedInvoiceIteminstances#itemsreturns a collection of associatedItemsby way ofInvoiceItemobjects#customerreturns an instance ofCustomerassociated with this object#merchantreturns an instance ofMerchantassociated with this object
InvoiceItem
#invoicereturns an instance ofInvoiceassociated with this object#itemreturns an instance ofItemassociated with this object
Item
#invoice_itemsreturns a collection ofInvoiceItemsassociated with this object#merchantreturns an instance ofMerchantassociated with this object
Transaction
#invoicereturns an instance ofInvoiceassociated with this object
Customer
#invoicesreturns a collection ofInvoiceinstances associated with this object.
Business Intelligence
Merchant
.most_revenue(x)returns the topxmerchant instances ranked by total revenue.most_items(x)returns the topxmerchant instances ranked by total number of items sold.revenue(date)returns the total revenue for that date across all merchants#revenuereturns the total revenue for that merchant across all transactions#revenue(date)returns the total revenue for that merchant for a specific invoice date#favorite_customerreturns theCustomerwho has conducted the most successful transactions#customers_with_pending_invoicesreturns a collection ofCustomerinstances which have pending (unpaid) invoices
NOTE: Failed charges should never be counted in revenue totals or statistics.
NOTE: All revenues should be reported as a BigDecimal object with two decimal places.
Item
.most_revenue(x)returns the topxitem instances ranked by total revenue generated.most_items(x)returns the topxitem instances ranked by total number sold#best_dayreturns the date with the most sales for the given item using the invoice date
Customer
#transactionsreturns an array ofTransactioninstances associated with the customer#favorite_merchantreturns an instance ofMerchantwhere the customer has conducted the most successful transactions
Invoice - Creating New Invoices & Related Objects
Given a hash of inputs, you can create new invoices on the fly using this syntax:
1 2 | |
Assuming that customer, merchant, and item1/item2/item3 are instances of their respective classes.
You should determine the quantity bought for each item by how many times the item is in the :items array.
So, for items: [item1, item1, item2], the quantity bought will be 2 for item1 and 1 for item2.
Then, on such an invoice you can call:
1 2 | |
The objects created through this process would then affect calculations, finds, etc.
Extensions
Merchant
Add these four methods to Merchant, qualifying as one extension:
.dates_by_revenuereturns an array of RubyDateobjects in descending order of revenue.dates_by_revenue(x)returns the topxdays of revenue in descending order.revenue(range_of_dates)returns the total revenue for all merchants across several dates#revenue(range_of_dates)returns the total revenue for that merchant across several dates
Invoice
Add these five methods to Invoice, qualifying as one extension:
.pendingreturns an array ofInvoiceinstances for which there is no successful transaction.average_revenuereturns aBigDecimalof the average total for each processed invoice.average_revenue(date)returns aBigDecimalof the average total for each processed invoice for a single date.average_itemsreturns aBigDecimalof the average item count for each processed invoice.average_items(date)returns aBigDecimalof the average item count for each processed invoice for a single date
NOTE: All BigDecimal objects should use two decimal places. "Processed invoice" refers to an invoice that has at least one successful transaction.
Customer
Add these four methods to Customer, qualifying as one extension:
#days_since_activityreturns a count of the days since their last transaction, zero means today.#pending_invoicesreturns an array ofInvoiceinstances for which there is no successful transaction.most_itemsreturns theCustomerwho has purchased the most items by quantity.most_revenuereturns theCustomerwho has generated the most total revenue
Evaluation Criteria
This project will be peer assessed using automated tests and the rubric below.
- Correctness
- 4: All provided tests pass without an error or crash
- 3: One test failed or crashed
- 2: Two or three tests failed or crashed
- 1: More than three tests failed or crashed
- 0: Program will not run
- Testing
- 4: Testing suite covers >95% of application code
- 3: Testing suite covers 90-94% of application code
- 2: Testing suite covers 80-89% of application code
- 1: Testing suite covers 50-89% of application code
- 0: Testing suite covers <50% of application code
- Style
- 4: Source code consistently uses strong code style including lines under 80 characters, methods under 10 lines of code, and correct indentation.
- 3: Source code uses good code style, but breaks the above criteria in three or fewer spots
- 2: Source code uses mixed style, with three to six style breaks
- 1: Source code is generally messy with six to twelve issues
- 0: Source code is unacceptable, containing more than twelve style issues
- Effort
- 5: Program fulfills all Base Expectations and three Extensions
- 4: Program fulfills all Base Expectations and two Extensions
- 3: Program fulfills all Base Expectations
- 2: Program fulfills Base Expectations except for one or two features.
- 1: Program fulfills some Base Expectations, but more than two features are broken.
- 0: Program does not fulfill any of the Base Expectations