<repo_name>nayeemzen/koin-merchant-android<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/checkout/CheckoutPresenter.java
package com.sendkoin.merchant.ui.checkout;

import com.annimon.stream.Stream;
import com.google.gson.Gson;
import com.pushtorefresh.storio.sqlite.operations.delete.DeleteResult;
import com.sendkoin.api.InitiateDynamicTransactionRequest;
import com.sendkoin.api.InitiateDynamicTransactionResponse;
import com.sendkoin.api.QrCode;
import com.sendkoin.api.SaleItem;
import com.sendkoin.events.TransactionProcessedEvent;
import com.sendkoin.merchant.data.onboarding.MerchantDataStore;
import com.sendkoin.merchant.data.transactions.SaleItemDataStore;
import com.sendkoin.merchant.data.transactions.TransactionService;
import com.sendkoin.sql.entities.MerchantEntity;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.util.List;

import javax.inject.Inject;

import okhttp3.WebSocket;
import rx.Observer;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import timber.log.Timber;

import static com.sendkoin.events.TransactionProcessedEvent.Status.SUCCESSFUL;

class CheckoutPresenter implements CheckoutContract.Presenter {
  private static final String TAG = CheckoutPresenter.class.getSimpleName();
  private final CheckoutContract.View view;
  private final SaleItemDataStore saleItemDataStore;
  private MerchantDataStore merchantDataStore;
  private final TransactionService transactionService;
  private WebSocket webSocket;
  private EventBus eventBus;
  private Gson gson;
  private final CompositeSubscription compositeSubscription = new CompositeSubscription();
  private String transactionToken;

  @Inject
  CheckoutPresenter(CheckoutContract.View view,
                    SaleItemDataStore saleItemDataStore,
                    MerchantDataStore merchantDataStore,
                    TransactionService transactionService,
                    WebSocket webSocket,
                    EventBus eventBus,
                    Gson gson) {
    this.view = view;
    this.saleItemDataStore = saleItemDataStore;
    this.merchantDataStore = merchantDataStore;
    this.transactionService = transactionService;
    this.webSocket = webSocket;
    this.eventBus = eventBus;
    this.gson = gson;
  }

  @Override
  public void subscribe() {
    eventBus.register(this);
    loadMerchant();
    loadCurrentSale();
  }

  @Override
  public void loadMerchant() {
    Subscription subscription = merchantDataStore.getOnly()
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(merchant -> {
          MerchantEntity merchantEntity = merchant.get();
          view.showInstruction(merchantEntity.getStoreName());
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void unsubscribe() {
    // Defined status codes - https://tools.ietf.org/html/rfc6455#section-7.4.
    eventBus.unregister(this);
    webSocket.close(1001, "Presenter::unsubscribe called");
    compositeSubscription.clear();
  }

  @Override
  public void loadCurrentSale() {
    Subscription subscription = saleItemDataStore.getAllItems()
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .doOnNext(items -> view.showCurrentSaleAmount(Stream.of(items)
            .mapToInt(item -> item.getPrice().intValue() * item.getQuantity().intValue())
            .sum()))
        .flatMap(items -> transactionService.initiateTransaction(
            new InitiateDynamicTransactionRequest.Builder()
                .sale_items(Stream.of(items).map(SaleItemDataStore::toWire).toList())
                .build())
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread()))
        .subscribe(new Observer<InitiateDynamicTransactionResponse>() {
          @Override
          public void onCompleted() {
          }

          @Override
          public void onError(Throwable e) {
            Timber.e(e, "Error in checkout");
          }

          @Override
          public void onNext(InitiateDynamicTransactionResponse initiateTransactionResponse) {
            transactionToken = initiateTransactionResponse.transaction_token;
            String qrCodeJson = gson.toJson(initiateTransactionResponse.qr_code, QrCode.class);
            view.showPaymentQrCode(qrCodeJson);
          }
        });

    compositeSubscription.add(subscription);
  }

  @Subscribe(threadMode = ThreadMode.MAIN)
  @Override
  public void onTransactionProcessed(TransactionProcessedEvent event) {
    Timber.d("Received event! [TransactionProcessedEvent=%s]", event);
    if (event.transaction_token.equals(transactionToken)) {
      view.showCheckoutProcessing();

      if (event.status != SUCCESSFUL) {
        view.showFailedCheckout(event.status, event.failure_reason);
        return;
      }

      Subscription subscription = saleItemDataStore.removeAllItems()
          .subscribeOn(Schedulers.io())
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe(new Observer<DeleteResult>() {
            @Override
            public void onCompleted() {
            }

            @Override
            public void onError(Throwable e) {
              Timber.e(e);
            }

            @Override
            public void onNext(DeleteResult deleteResult) {
              view.showSuccessfulCheckout();
            }
          });

      compositeSubscription.add(subscription);
    }
  }

  static final class SaleSummary {
    final List<SaleItem> saleItems;
    final String merchantName;

    SaleSummary(List<SaleItem> saleItems, String merchantName) {
      this.saleItems = saleItems;
      this.merchantName = merchantName;
    }
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/orders/list/OrderListContract.java
package com.sendkoin.merchant.ui.orders.list;

import com.sendkoin.merchant.ui.BasePresenter;
import com.sendkoin.sql.entities.KoinOrderEntity;

import java.util.List;

public interface OrderListContract {
  interface View {
    void showPendingOrders(List<KoinOrderEntity> orderEntities);

    void showOrderDetails(KoinOrderEntity order);
  }

  interface Presenter extends BasePresenter {
    void loadPendingOrdersFromLocal();

    void loadNewPendingOrdersFromServer();
  }
}
<file_sep>/app/src/main/sql/merchant.sql
CREATE TABLE Merchant (
    id INTEGER NOT NULL PRIMARY KEY,
    store_name TEXT NOT NULL,
    store_location TEXT NOT NULL,
    store_type TEXT NOT NULL,
    phone_number TEXT NULL DEFAULT NULL,
    email_address TEXT NULL DEFAULT NULL
);<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/orders/list/OrderListFragment.java
package com.sendkoin.merchant.ui.orders.list;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.sendkoin.merchant.KoinMerchantApp;
import com.sendkoin.merchant.R;
import com.sendkoin.merchant.data.orders.OrderDataStore;
import com.sendkoin.merchant.ui.MainActivity;
import com.sendkoin.merchant.ui.orders.details.OrderDetailsFragment;
import com.sendkoin.merchant.ui.utils.DividerItemDecoration;
import com.sendkoin.sql.entities.KoinOrderEntity;

import java.util.List;

import javax.inject.Inject;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;

import static android.support.v7.widget.LinearLayoutManager.VERTICAL;

public class OrderListFragment extends Fragment implements OrderListContract.View {
  @BindView(R.id.rv_transactions_pending_review) RecyclerView recyclerView;
  @BindView(R.id.tv_empty_activity) TextView emptyTextView;

  @Inject OrderListContract.Presenter presenter;

  private Unbinder unbinder;
  private PendingOrdersListAdapter recyclerViewAdapter;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater,
                           @Nullable ViewGroup container,
                           @Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    View view = inflater.inflate(R.layout.order_list_fragment, container, false);
    unbinder = ButterKnife.bind(this, view);
    setHasOptionsMenu(true);
    return view;
  }

  @Override
  public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ActionBar supportActionBar = ((MainActivity) getActivity()).getSupportActionBar();
    supportActionBar.setTitle("Sales");
    supportActionBar.show();
    setUpDagger();
    setUpRecyclerView();
  }

  @Override
  public void showPendingOrders(List<KoinOrderEntity> pendingOrders) {
    if (pendingOrders.isEmpty()) {
      recyclerView.setVisibility(View.GONE);
      emptyTextView.setVisibility(View.VISIBLE);
    } else {
      emptyTextView.setVisibility(View.GONE);
      recyclerView.setVisibility(View.VISIBLE);
      recyclerViewAdapter.setPendingOrdersList(pendingOrders);
    }
  }

  private void setUpRecyclerView() {
    LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), VERTICAL, false);
    recyclerView.addItemDecoration(new DividerItemDecoration(getActivity()));
    recyclerView.setLayoutManager(layoutManager);
    recyclerView.setHasFixedSize(true);
    recyclerViewAdapter = new PendingOrdersListAdapter(this);
    recyclerView.setAdapter(recyclerViewAdapter);
  }

  @Override
  public void onResume() {
    super.onResume();
    presenter.subscribe();
  }

  @Override
  public void onPause() {
    super.onPause();
    presenter.unsubscribe();
  }

  @Override
  public void onDestroyView() {
    unbinder.unbind();
    super.onDestroyView();
  }

  private void setUpDagger() {
    DaggerOrderListComponent.builder()
        .orderListModule(new OrderListModule(this))
        .netComponent(((KoinMerchantApp) getActivity()
            .getApplicationContext())
            .getNetComponent())
        .build()
        .inject(this);
  }

  @Override
  public void showOrderDetails(KoinOrderEntity order) {
    Bundle bundle = new Bundle();
    bundle.putLong("order_details.local_id", order.getLocalId());
    bundle.putByteArray("order_details.order", OrderDataStore.toWire(order).encode());
    OrderDetailsFragment orderDetailsFragment = new OrderDetailsFragment();
    orderDetailsFragment.setArguments(bundle);

    getActivity().getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.fragment_placeholder, orderDetailsFragment)
        .addToBackStack(null)
        .commit();
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/review/TransactionReviewPresenter.java
package com.sendkoin.merchant.ui.transactions.review;

import com.annimon.stream.Stream;
import com.pushtorefresh.storio.sqlite.operations.delete.DeleteResult;
import com.sendkoin.merchant.data.transactions.SaleItemDataStore;
import com.sendkoin.sql.entities.SaleItemEntity;

import java.util.List;

import javax.inject.Inject;

import rx.Observer;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import timber.log.Timber;

import static com.annimon.stream.Collectors.summingInt;

class TransactionReviewPresenter implements TransactionReviewContract.Presenter {
  private static final String TAG = TransactionReviewPresenter.class.getSimpleName();
  private final SaleItemDataStore saleItemDataStore;

  private TransactionReviewContract.View view;
  private CompositeSubscription compositeSubscription = new CompositeSubscription();

  @Inject
  TransactionReviewPresenter(TransactionReviewContract.View view,
                             SaleItemDataStore saleItemDataStore) {
    this.view = view;
    this.saleItemDataStore = saleItemDataStore;
  }

  @Override
  public void subscribe() {
    loadItems();
  }

  @Override
  public void loadItems() {
    Subscription subscription = saleItemDataStore.getAllItems()
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<List<SaleItemEntity>>() {
          @Override
          public void onCompleted() {
            Timber.d("Load items complete!");
          }

          @Override
          public void onError(Throwable e) {
            Timber.e(e, "Load items failed!");
          }

          @Override
          public void onNext(List<SaleItemEntity> saleItems) {
            // TODO(Zen): Fix duplicated and inefficient summing logic in
            // TransactionReviewPresenter and CurrentSalePresenter.
            Integer total = Stream.of(saleItems).collect(
                summingInt(i -> i.getPrice().intValue() * i.getQuantity().intValue()));
            view.showCurrentSaleAmount(total);
            view.showNumItems(saleItems.size());
            view.showTransactionReviewItems(saleItems);
          }
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void deleteItem(SaleItemEntity saleItem) {
    Subscription subscription = saleItemDataStore.removeItem(saleItem.getId())
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<DeleteResult>() {
          @Override
          public void onCompleted() {
            Timber.d("Delete item [id=%s] complete!", saleItem.getId());
          }

          @Override
          public void onError(Throwable e) {
            Timber.d("Delete item [id=%s] failed!", saleItem.getId());
          }

          @Override
          public void onNext(DeleteResult deleteResult) {
            view.showItemDeleted(saleItem.getTitle());
          }
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void unsubscribe() {
    compositeSubscription.clear();
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/data/onboarding/MerchantService.java
package com.sendkoin.merchant.data.onboarding;


import com.sendkoin.api.Merchant;
import com.sendkoin.api.OnboardMerchantRequest;
import com.sendkoin.api.OnboardMerchantResponse;

import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import rx.Observable;

public interface MerchantService {
  @POST("merchant/onboard")
  Observable<OnboardMerchantResponse> onboardMerchant(
      @Body OnboardMerchantRequest onboardMerchantRequest);

  @GET("merchant")
  Observable<Merchant> getMerchant();
}
<file_sep>/app/src/test/java/com/sendkoin/merchant/ui/currentsale/CurrentSaleTestComponent.java
package com.sendkoin.merchant.ui.currentsale;

import com.sendkoin.merchant.KoinTestComponent;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Component;

@CustomScope
@Component(dependencies = KoinTestComponent.class, modules = CurrentSaleModule.class)
public interface CurrentSaleTestComponent {
  void inject(CurrentSaleTest currentSaleTest);
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/history/TransactionHistoryModule.java
package com.sendkoin.merchant.ui.transactions.history;


import com.sendkoin.merchant.data.transactions.TransactionDataStore;
import com.sendkoin.merchant.data.transactions.TransactionRepository;
import com.sendkoin.merchant.data.transactions.TransactionService;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Module;
import dagger.Provides;
import retrofit2.Retrofit;

@Module
public class TransactionHistoryModule {
  private TransactionHistoryContract.View view;

  public TransactionHistoryModule(TransactionHistoryContract.View view) {
    this.view = view;
  }

  @Provides
  @CustomScope
  public TransactionHistoryContract.View provideTransactionHistoryView() {
    return view;
  }

  @Provides
  @CustomScope
  public TransactionService transactionService(Retrofit retrofit) {
    return retrofit.create(TransactionService.class);
  }

  @Provides
  @CustomScope
  public TransactionRepository provideTransactionRepository(
      TransactionService transactionService, TransactionDataStore transactionDataStore) {
    return new TransactionRepository(transactionDataStore, transactionService);
  }

  @Provides
  @CustomScope
  public TransactionHistoryContract.Presenter provideTransactionHistoryPresenter(
      TransactionHistoryContract.View view,
      TransactionRepository transactionRepository) {
    return new TransactionHistoryPresenter(view, transactionRepository);
  }
}
<file_sep>/app/src/main/sql/transaction.sql
CREATE TABLE KoinTransaction (
    transaction_token TEXT PRIMARY KEY NOT NULL,
    amount INTEGER NOT NULL,
    created_at INTEGER NOT NULL,
    state TEXT NOT NULL
);<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/orders/details/OrderDetailsComponent.java
package com.sendkoin.merchant.ui.orders.details;

import com.sendkoin.merchant.dagger.component.NetComponent;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Component;

@CustomScope
@Component(dependencies = NetComponent.class, modules = OrderDetailsModule.class)
public interface OrderDetailsComponent {
  void inject(OrderDetailsFragment orderDetailsFragment);
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/review/TransactionReviewContract.java
package com.sendkoin.merchant.ui.transactions.review;

import com.sendkoin.merchant.ui.BasePresenter;
import com.sendkoin.sql.entities.SaleItemEntity;

import java.util.List;

public class TransactionReviewContract {
  public interface View {
    void showCurrentSaleAmount(int currentSaleAmount);

    void showTransactionReviewItems(List<SaleItemEntity> items);

    void showItemDeleted(String itemName);

    void showNumItems(int numItems);
  }

  interface Presenter extends BasePresenter {
    void loadItems();

    void deleteItem(SaleItemEntity item);
  }
}
<file_sep>/app/src/main/sql/order_items.sql
CREATE TABLE OrderItem (
    id INTEGER NOT NULL PRIMARY KEY,
    order_id INTEGER NOT NULL,
    order_item_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    price INTEGER NOT NULL,
    quantity INTEGER NOT NULL,
    customer_notes TEXT NULL DEFAULT NULL,
    sale_type TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL
);<file_sep>/app/src/main/java/com/sendkoin/merchant/data/notifications/RegisterDeviceService.java
package com.sendkoin.merchant.data.notifications;

import com.sendkoin.api.RegisterDeviceRequest;
import com.sendkoin.api.RegisterDeviceResponse;

import retrofit2.http.Body;
import retrofit2.http.POST;
import rx.Observable;

public interface RegisterDeviceService {
  @POST("register-device/merchant/firebase")
  Observable<RegisterDeviceResponse> registerDevice(@Body RegisterDeviceRequest request);
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/onboarding/OnboardingPresenter.java
package com.sendkoin.merchant.ui.onboarding;

import com.sendkoin.api.Merchant;
import com.sendkoin.api.OnboardMerchantRequest;
import com.sendkoin.merchant.data.onboarding.MerchantDataStore;
import com.sendkoin.merchant.data.onboarding.MerchantService;

import javax.inject.Inject;

import rx.Observer;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import timber.log.Timber;

class OnboardingPresenter implements OnboardingContract.Presenter {
  private static final String TAG = OnboardingPresenter.class.getSimpleName();
  private OnboardingContract.View view;
  private MerchantService merchantService;
  private MerchantDataStore merchantDataStore;
  private CompositeSubscription compositeSubscription = new CompositeSubscription();

  @Inject
  public OnboardingPresenter(OnboardingContract.View view,
                             MerchantService merchantService,
                             MerchantDataStore merchantDataStore) {
    this.view = view;
    this.merchantService = merchantService;
    this.merchantDataStore = merchantDataStore;
  }

  @Override
  public void subscribe() {
  }

  @Override
  public void unsubscribe() {
    compositeSubscription.clear();
  }

  @Override
  public void saveOnboardingData(Merchant merchant) {
    view.showResultLoading();
    OnboardMerchantRequest onboardMerchantRequest = new OnboardMerchantRequest.Builder()
        .store_name(merchant.store_name)
        .store_location(merchant.store_location)
        .store_type(merchant.store_type)
        .build();

    Subscription subscription = merchantService.onboardMerchant(onboardMerchantRequest)
        .subscribeOn(Schedulers.io())
        .flatMap(onboardMerchantResponse ->
            merchantDataStore.createOrUpdate(onboardMerchantResponse.merchant))
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<Merchant>() {
          @Override
          public void onCompleted() {
          }

          @Override
          public void onError(Throwable e) {
            Timber.d(e, "Error saving onboarding to server");
            view.showOnboardingFailure();
          }

          @Override
          public void onNext(Merchant merchant1) {
            view.showOnboardingSuccess();
          }
        });

    compositeSubscription.add(subscription);
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/checkout/CheckoutComponent.java
package com.sendkoin.merchant.ui.checkout;

import com.sendkoin.merchant.dagger.component.NetComponent;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Component;

@CustomScope
@Component(dependencies = NetComponent.class, modules = CheckoutModule.class)
public interface CheckoutComponent {
  void inject(CheckoutFragment fragment);
}
<file_sep>/app/src/main/sql/saleitem.sql
CREATE TABLE SaleItem (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    price INTEGER NOT NULL,
    quantity INTEGER NOT NULL,
    sale_type TEXT NULL DEFAULT NULL,
);<file_sep>/app/src/main/java/com/sendkoin/merchant/KoinFirebaseMessagingService.java
package com.sendkoin.merchant;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.sendkoin.merchant.data.notifications.DaggerKoinFirebaseComponent;
import com.sendkoin.merchant.data.notifications.KoinFirebaseModule;
import com.sendkoin.merchant.data.notifications.PushDataProcessor;
import com.sendkoin.merchant.ui.MainActivity;

import javax.inject.Inject;

public class KoinFirebaseMessagingService extends FirebaseMessagingService {
  @Inject PushDataProcessor pushDataProcessor;

  @Override
  public void onCreate() {
    setUpDagger();
  }

  @Override
  public void onMessageReceived(RemoteMessage remoteMessage) {
    String description = remoteMessage.getData().get("description");
    String data = remoteMessage.getData().get("data");
    String type = remoteMessage.getData().get("type");
    pushDataProcessor.process(type, data);
    showNotification(description);
  }

  private void setUpDagger() {
    DaggerKoinFirebaseComponent.builder()
        .netComponent(((KoinMerchantApp) getApplicationContext()).getNetComponent())
        .koinFirebaseModule(new KoinFirebaseModule())
        .build()
        .inject(this);
  }

  @Override
  public void onDeletedMessages() {
    super.onDeletedMessages();
  }

  private void showNotification(String description) {
    Intent intent = new Intent(this, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
        PendingIntent.FLAG_ONE_SHOT);

    NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle()
        .setBigContentTitle("Koin Merchant")
        .setSummaryText(description);

    NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext())
        .setSmallIcon(R.drawable.ic_add_tag)
        .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary))
        .setContentTitle("Koin Merchant")
        .setContentText(description)
        .setAutoCancel(true)
        .setLights(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary), 100, 1900)
        .setPriority(NotificationCompat.PRIORITY_HIGH)
        .setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE)
        .setStyle(inboxStyle);

    NotificationManager notificationManager =
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
  }
}

<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/history/TransactionHistoryContract.java
package com.sendkoin.merchant.ui.transactions.history;


import com.sendkoin.api.Transaction;
import com.sendkoin.merchant.ui.BasePresenter;

import java.util.List;

public interface TransactionHistoryContract {
  interface View {
    void showTransactions(List<Transaction> transactions);

    void addTransactions(List<Transaction> transactions);
  }

  interface Presenter extends BasePresenter {
    void loadNewTransactionsFromServer();

    void loadTransactionsFromLocal();

    void loadOldTransactionsFromServer();
  }
}
<file_sep>/app/src/test/java/com/sendkoin/merchant/ui/currentsale/CurrentSaleTest.java
package com.sendkoin.merchant.ui.currentsale;

import com.sendkoin.api.SaleItem;
import com.sendkoin.merchant.DaggerKoinTestComponent;
import com.sendkoin.merchant.KoinTestModule;
import com.sendkoin.merchant.RxSchedulersOverrideRule;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import javax.inject.Inject;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@RunWith(RobolectricTestRunner.class)
@Config(manifest="src/main/AndroidManifest.xml")
public class CurrentSaleTest {
  @Inject CurrentSaleContract.View currentSaleView;
  @Inject CurrentSaleContract.Presenter currentSalePresenter;

  @Rule public RxSchedulersOverrideRule rxSchedulersOverrideRule = new RxSchedulersOverrideRule();

  @Before
  public void setUp() {
    DaggerCurrentSaleTestComponent.builder()
        .koinTestComponent(DaggerKoinTestComponent.builder()
            .koinTestModule(new KoinTestModule())
            .build())
        .currentSaleModule(new CurrentSaleModule(mock(CurrentSaleFragment.class)))
        .build()
        .inject(this);
  }

  @Test
  public void quickSaleItemsLoadedInCurrentSaleView() throws Exception {
    currentSalePresenter.loadItems();

    currentSalePresenter.addItem(new SaleItem.Builder()
        .sale_type(SaleItem.SaleType.QUICK_SALE)
        .name("Coffee")
        .price(300)
        .quantity(2)
        .build());

    currentSalePresenter.addItem(new SaleItem.Builder()
        .sale_type(SaleItem.SaleType.QUICK_SALE)
        .name("Red Velvet Cake")
        .price(500)
        .quantity(1)
        .build());

    verify(currentSaleView).showTotalAmount(1100);
  }

  @Test
  public void quickSaleNumPadUpdatesView() throws Exception {
    // Add item with price 750, quantity 10.
    currentSalePresenter.numPadClicked("7", true, false);
    currentSalePresenter.numPadClicked("5", true, false);
    currentSalePresenter.numPadClicked("0", true, false);
    currentSalePresenter.numPadClicked("0", false, true);

    verify(currentSaleView).showPrice("750");
    verify(currentSaleView).showQuantity("10");

    // Remove the price and quantity.
    currentSalePresenter.numPadClicked("<", true, false);
    currentSalePresenter.numPadClicked("<", true, false);
    currentSalePresenter.numPadClicked("<", true, false);
    verify(currentSaleView, times(2)).showPrice("75");
    verify(currentSaleView, times(2)).showPrice("7");
    verify(currentSaleView).showPrice("");

    currentSalePresenter.numPadClicked("<", false, true);
    currentSalePresenter.numPadClicked("<", false, true);
    currentSalePresenter.numPadClicked("<", false, true);
    verify(currentSaleView).showQuantity("1");
    verify(currentSaleView).showQuantity("");

    // Add item with price 5 and quantity 5.
    currentSalePresenter.numPadClicked("5", true, false);
    currentSalePresenter.numPadClicked("5", false, true);
    verify(currentSaleView).showPrice("5");
    verify(currentSaleView).showQuantity("5");
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/home/summary/SummaryContact.java
package com.sendkoin.merchant.ui.home.summary;

import com.sendkoin.merchant.ui.BasePresenter;

public interface SummaryContact {
  interface View {

  }

  interface Presenter extends BasePresenter {

  }
}
<file_sep>/app/src/main/sql/order.sql
CREATE TABLE KoinOrder (
    local_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    order_id INTEGER NOT NULL,
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL,
    transaction_token TEXT NOT NULL,
    amount INTEGER NOT NULL,
    state TEXT NOT NULL
);<file_sep>/app/src/main/java/com/sendkoin/merchant/data/orders/OrderDataStore.java
package com.sendkoin.merchant.data.orders;

import com.annimon.stream.Stream;
import com.pushtorefresh.storio.sqlite.StorIOSQLite;
import com.pushtorefresh.storio.sqlite.operations.delete.DeleteResult;
import com.pushtorefresh.storio.sqlite.operations.put.PutResults;
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery;
import com.pushtorefresh.storio.sqlite.queries.Query;
import com.sendkoin.api.Order;
import com.sendkoin.api.OrderState;
import com.sendkoin.sql.entities.KoinOrderEntity;
import com.sendkoin.sql.entities.OrderItemEntity;
import com.sendkoin.sql.tables.KoinOrderTable;
import com.sendkoin.sql.tables.OrderItemTable;

import java.util.List;

import rx.Observable;

public class OrderDataStore {
  private final StorIOSQLite storIOSQLite;

  public OrderDataStore(StorIOSQLite storIOSQLite) {
    this.storIOSQLite = storIOSQLite;
  }

  public Observable<PutResults<KoinOrderEntity>> createOrUpdate(List<Order> orders) {
    return storIOSQLite.put()
        .objects(Stream.of(orders)
            .map(OrderDataStore::fromWire)
            .toList())
        .withPutResolver(new KoinOrderEntityCustomPutResolver())
        .prepare()
        .asRxObservable();
  }

  public Observable<List<KoinOrderEntity>> getAllOrders() {
    return storIOSQLite.get()
        .listOfObjects(KoinOrderEntity.class)
        .withQuery(Query.builder()
            .table(KoinOrderTable.TABLE)
            .orderBy(KoinOrderTable.COLUMN_UPDATED_AT + " DESC")
            .build())
        .prepare()
        .asRxObservable();
  }

  public Observable<List<OrderItemEntity>> getOrderDetails(long orderId) {
    return storIOSQLite.get()
        .listOfObjects(OrderItemEntity.class)
        .withQuery(Query.builder()
            .table(OrderItemTable.TABLE)
            .where(OrderItemTable.COLUMN_ORDER_ID + " = ?")
            .whereArgs(orderId)
            .build())
        .prepare()
        .asRxObservable();
  }

  public static KoinOrderEntity fromWire(Order order) {
    return new KoinOrderEntity(
        null,
        order.order_id,
        order.created_at,
        order.updated_at,
        order.transaction.token,
        order.transaction.amount.longValue(),
        order.state.name());
  }

  public Observable<KoinOrderEntity> getLatestOrder() {
    return storIOSQLite.get()
        .object(KoinOrderEntity.class)
        .withQuery(Query.builder()
            .table(KoinOrderTable.TABLE)
            .limit(1)
            .orderBy(KoinOrderTable.COLUMN_UPDATED_AT + " DESC")
            .build())
        .prepare()
        .asRxObservable();
  }

  public Observable<DeleteResult> deleteOrder(long localOrderId) {
    return storIOSQLite.delete()
        .byQuery(DeleteQuery.builder()
            .table(KoinOrderTable.TABLE)
            .where(KoinOrderTable.COLUMN_LOCAL_ID + " = ?")
            .whereArgs(localOrderId)
            .build())
        .prepare()
        .asRxObservable();
  }

  public static Order toWire(KoinOrderEntity orderEntity) {
    return new Order.Builder()
        .order_id(orderEntity.getOrderId())
        .state(OrderState.valueOf(orderEntity.getState()))
        .created_at(orderEntity.getCreatedAt())
        .updated_at(orderEntity.getUpdatedAt())
        .build();
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/history/TransactionHistoryPresenter.java
package com.sendkoin.merchant.ui.transactions.history;

import com.annimon.stream.Stream;
import com.pushtorefresh.storio.sqlite.operations.put.PutResults;
import com.sendkoin.api.ListTransactionsResponse;
import com.sendkoin.api.QueryParameters;
import com.sendkoin.api.TransactionState;
import com.sendkoin.merchant.data.transactions.TransactionDataStore;
import com.sendkoin.merchant.data.transactions.TransactionRepository;
import com.sendkoin.sql.entities.KoinTransactionEntity;

import java.util.List;

import javax.inject.Inject;

import rx.Observer;
import rx.Subscription;
import rx.subscriptions.CompositeSubscription;
import timber.log.Timber;

import static com.sendkoin.api.QueryParameters.Order.ASCENDING;
import static com.sendkoin.api.QueryParameters.Order.DESCENDING;

class TransactionHistoryPresenter implements TransactionHistoryContract.Presenter {
  private final CompositeSubscription compositeSubscription = new CompositeSubscription();
  private final TransactionHistoryContract.View view;
  private TransactionRepository transactionRepository;

  @Inject
  TransactionHistoryPresenter(TransactionHistoryContract.View view,
                              TransactionRepository transactionRepository) {
    this.view = view;
    this.transactionRepository = transactionRepository;
  }

  /**
   * Do these steps in parallel.
   * 1. Load all transactions from the local database.
   * 2. Load new transactions from the server and insert into the database.
   */
  @Override
  public void subscribe() {
    loadTransactionsFromLocal();
    loadNewTransactionsFromServer();
  }

  @Override
  public void loadTransactionsFromLocal() {
    Subscription subscription = transactionRepository.loadTransactionsFromLocal()
        .subscribe(new Observer<List<KoinTransactionEntity>>() {
          @Override
          public void onCompleted() {
            Timber.d("Completed loadTransactionsFromLocal().");
          }

          @Override
          public void onError(Throwable e) {
            Timber.e(e);
          }

          @Override
          public void onNext(List<KoinTransactionEntity> transactionEntities) {
            Timber.d("Loaded %d orderEntity entities.", transactionEntities.size());
            view.showTransactions(Stream.of(transactionEntities)
                .map(TransactionDataStore::toWire)
                .toList());
          }
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void loadNewTransactionsFromServer() {
    Subscription subscription = transactionRepository.loadNewTransactionsFromServer(
        new QueryParameters.Builder()
            .order(ASCENDING)
            .transaction_state(TransactionState.PROCESSING)
            .build())
        .subscribe(new Observer<PutResults<KoinTransactionEntity>>() {
          @Override
          public void onCompleted() {
            Timber.d("loadNewPendingOrdersFromServer complete.");
          }

          @Override
          public void onError(Throwable e) {
            Timber.e(e);
          }

          @Override
          public void onNext(PutResults<KoinTransactionEntity> putResults) {
            Timber.d(
                "Fetched from server and locally saved %d transactions, updated %d transactions",
                putResults.numberOfInserts(),
                putResults.numberOfUpdates());
          }
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void loadOldTransactionsFromServer() {
    Subscription subscription = transactionRepository.loadOldTransactionsFromServer(
        new QueryParameters.Builder()
            .order(DESCENDING)
            .transaction_state(TransactionState.PROCESSING)
            .build())
        .subscribe(new Observer<ListTransactionsResponse>() {
          @Override
          public void onCompleted() {
            Timber.d("loadNewPendingOrdersFromServer complete.");
          }

          @Override
          public void onError(Throwable e) {
            Timber.e(e);
          }

          @Override
          public void onNext(ListTransactionsResponse listTransactionsResponse) {
            Timber.d("Fetched %d old transactions from server.",
                listTransactionsResponse.transactions.size());
            view.addTransactions(listTransactionsResponse.transactions);
          }
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void unsubscribe() {
    compositeSubscription.clear();
  }

}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/orders/list/OrderListPresenter.java
package com.sendkoin.merchant.ui.orders.list;

import com.pushtorefresh.storio.sqlite.operations.put.PutResults;
import com.sendkoin.api.QueryParameters;
import com.sendkoin.merchant.data.orders.OrderRepository;
import com.sendkoin.sql.entities.KoinOrderEntity;

import java.util.List;

import javax.inject.Inject;

import rx.Observer;
import rx.Subscription;
import rx.subscriptions.CompositeSubscription;
import timber.log.Timber;

import static com.sendkoin.api.OrderState.ORDER_PROCESSING;
import static com.sendkoin.api.QueryParameters.Order.ASCENDING;

public class OrderListPresenter implements OrderListContract.Presenter {
  private final OrderListContract.View view;
  private final CompositeSubscription compositeSubscription = new CompositeSubscription();
  private final OrderRepository orderRepository;

  @Inject
  public OrderListPresenter(OrderListContract.View view, OrderRepository orderRepository) {
    this.view = view;
    this.orderRepository = orderRepository;
  }

  @Override
  public void subscribe() {
    loadNewPendingOrdersFromServer();
    loadPendingOrdersFromLocal();
  }

  @Override
  public void loadPendingOrdersFromLocal() {
    Subscription subscription = orderRepository.loadOrdersFromLocal()
        .subscribe(new Observer<List<KoinOrderEntity>>() {
          @Override
          public void onCompleted() {
            Timber.d("Completed loadPendingOrdersFromLocal().");
          }

          @Override
          public void onError(Throwable e) {
            Timber.e(e);
          }

          @Override
          public void onNext(List<KoinOrderEntity> orderEntities) {
            Timber.d("Loaded %d order entities.", orderEntities.size());
            view.showPendingOrders(orderEntities);
          }
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void loadNewPendingOrdersFromServer() {
    Subscription subscription = orderRepository.loadNewOrdersFromServer(new QueryParameters.Builder()
        .order(ASCENDING)
        .order_state(ORDER_PROCESSING)
        .build())
        .subscribe(new Observer<PutResults<KoinOrderEntity>>() {
          @Override
          public void onCompleted() {
            Timber.d("loadNewPendingOrdersFromServer complete.");
          }

          @Override
          public void onError(Throwable e) {
            Timber.e(e);
          }

          @Override
          public void onNext(PutResults<KoinOrderEntity> putResults) {
            Timber.d(
                "Fetched new orders from server and locally saved %d orders, updated %d orders",
                putResults.numberOfInserts(),
                putResults.numberOfUpdates());
          }
        });

    compositeSubscription.add(subscription);
  }

  @Override
  public void unsubscribe() {
    compositeSubscription.clear();
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/onboarding/OnboardingContract.java
package com.sendkoin.merchant.ui.onboarding;

import com.sendkoin.api.Merchant;
import com.sendkoin.merchant.ui.BasePresenter;

public interface OnboardingContract {
  interface View {
    void onBackPressed();

    boolean canGoBack();

    void showOnboardingSuccess();

    void showOnboardingFailure();

    void showResultLoading();
  }

  interface Presenter extends BasePresenter {
    void saveOnboardingData(Merchant merchant);
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/currentsale/CurrentSaleModule.java
package com.sendkoin.merchant.ui.currentsale;

import com.sendkoin.merchant.data.transactions.SaleItemDataStore;
import com.sendkoin.merchant.ui.CustomScope;
import com.sendkoin.merchant.ui.currentsale.CurrentSaleContract.Presenter;
import com.sendkoin.merchant.ui.currentsale.CurrentSaleContract.View;

import dagger.Module;
import dagger.Provides;

@Module
public class CurrentSaleModule {
  private final View view;

  public CurrentSaleModule(View view) {
    this.view = view;
  }

  @Provides
  @CustomScope
  View provideCurrentSaleView() {
    return view;
  }

  @Provides
  @CustomScope
  Presenter provideCurrentSalePresenter(View view,
                                        SaleItemDataStore saleItemDataStore) {
    return new CurrentSalePresenter(view, saleItemDataStore);
  }
}
<file_sep>/app/src/test/java/com/sendkoin/merchant/ui/transactions/review/TransactionReviewTest.java
package com.sendkoin.merchant.ui.transactions.review;


import com.annimon.stream.Stream;
import com.google.common.collect.ImmutableList;
import com.sendkoin.api.SaleItem;
import com.sendkoin.merchant.DaggerKoinTestComponent;
import com.sendkoin.merchant.KoinTestModule;
import com.sendkoin.merchant.RxSchedulersOverrideRule;
import com.sendkoin.merchant.data.transactions.SaleItemDataStore;
import com.sendkoin.merchant.ui.currentsale.CurrentSaleContract;
import com.sendkoin.merchant.ui.currentsale.CurrentSaleFragment;
import com.sendkoin.merchant.ui.currentsale.CurrentSaleModule;
import com.sendkoin.sql.entities.SaleItemEntity;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.List;

import javax.inject.Inject;

import static com.sendkoin.api.SaleItem.SaleType.QUICK_SALE;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@RunWith(RobolectricTestRunner.class)
@Config(manifest = "src/main/AndroidManifest.xml")
public class TransactionReviewTest {
  @Captor ArgumentCaptor<List<SaleItemEntity>> argumentCaptor;

  @Inject CurrentSaleContract.Presenter currentSalePresenter;
  @Inject TransactionReviewContract.View transactionReviewView;
  @Inject TransactionReviewContract.Presenter transactionReviewPresenter;

  @Rule public RxSchedulersOverrideRule rxSchedulersOverrideRule = new RxSchedulersOverrideRule();

  @Before
  public void setUp() {
    DaggerTransactionReviewTestComponent.builder()
        .koinTestComponent(DaggerKoinTestComponent.builder()
            .koinTestModule(new KoinTestModule())
            .build())
        .transactionReviewModule(new TransactionReviewModule(mock(TransactionReviewFragment.class)))
        .currentSaleModule(new CurrentSaleModule(mock(CurrentSaleFragment.class)))
        .build()
        .inject(this);

    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void transactionReviewDisplaysItemsAndSummary() throws Exception {
    SaleItem milkShake = new SaleItem.Builder()
        .name("MilkShake")
        .price(200)
        .quantity(4)
        .sale_type(QUICK_SALE)
        .build();

    SaleItem cheeseBurger = new SaleItem.Builder()
        .name("<NAME>")
        .price(500)
        .quantity(4)
        .sale_type(QUICK_SALE)
        .build();

    transactionReviewPresenter.loadItems();
    currentSalePresenter.addItem(milkShake);
    currentSalePresenter.addItem(cheeseBurger);

    verify(transactionReviewView).showCurrentSaleAmount(2800);
    verify(transactionReviewView).showNumItems(2);
    verify(transactionReviewView, times(3)).showTransactionReviewItems(argumentCaptor.capture());
    List<SaleItemEntity> saleItemEntities = argumentCaptor.getValue();
    assertThat(Stream.of(saleItemEntities).map(SaleItemDataStore::toWire).toList())
        .isEqualTo(ImmutableList.of(milkShake, cheeseBurger));
  }

  @Test
  public void removingItemsUpdatesSummary() throws Exception {
    SaleItem milkShake = new SaleItem.Builder()
        .name("MilkShake")
        .price(200)
        .quantity(4)
        .sale_type(QUICK_SALE)
        .build();

    SaleItem cheeseBurger = new SaleItem.Builder()
        .name("<NAME>")
        .price(500)
        .quantity(4)
        .sale_type(QUICK_SALE)
        .build();

    transactionReviewPresenter.loadItems();
    verify(transactionReviewView).showNumItems(0);
    verify(transactionReviewView).showCurrentSaleAmount(0);

    currentSalePresenter.addItem(milkShake);
    verify(transactionReviewView).showCurrentSaleAmount(800);
    verify(transactionReviewView).showNumItems(1);

    currentSalePresenter.addItem(cheeseBurger);
    verify(transactionReviewView).showNumItems(2);
    verify(transactionReviewView).showCurrentSaleAmount(2800);

    verify(transactionReviewView, times(3)).showTransactionReviewItems(argumentCaptor.capture());
    List<SaleItemEntity> saleItemEntities = argumentCaptor.getValue();
    assertThat(Stream.of(saleItemEntities).map(SaleItemDataStore::toWire).toList())
        .isEqualTo(ImmutableList.of(milkShake, cheeseBurger));

    transactionReviewPresenter.deleteItem(saleItemEntities.get(0));
    verify(transactionReviewView, times(2)).showNumItems(1);
    verify(transactionReviewView).showCurrentSaleAmount(2000);
    verify(transactionReviewView, times(4)).showTransactionReviewItems(argumentCaptor.capture());
    saleItemEntities = argumentCaptor.getValue();
    assertThat(Stream.of(saleItemEntities).map(SaleItemDataStore::toWire).toList())
        .isEqualTo(ImmutableList.of(cheeseBurger));

    transactionReviewPresenter.deleteItem(saleItemEntities.get(0));
    verify(transactionReviewView, times(2)).showNumItems(0);
    verify(transactionReviewView, times(2)).showCurrentSaleAmount(0);
    verify(transactionReviewView, times(5)).showTransactionReviewItems(argumentCaptor.capture());
    saleItemEntities = argumentCaptor.getValue();
    assertThat(Stream.of(saleItemEntities).map(SaleItemDataStore::toWire).toList())
        .isEqualTo(ImmutableList.of());
  }
}<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/review/TransactionReviewModule.java
package com.sendkoin.merchant.ui.transactions.review;

import com.sendkoin.merchant.data.transactions.SaleItemDataStore;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Module;
import dagger.Provides;

@Module
public class TransactionReviewModule {
  private final TransactionReviewContract.View view;

  public TransactionReviewModule(TransactionReviewContract.View view) {
    this.view = view;
  }

  @Provides
  @CustomScope
  TransactionReviewContract.View providesTransactionReviewScreenView() {
    return view;
  }

  @Provides
  @CustomScope
  TransactionReviewContract.Presenter providesTransactionReviewScreenPresenter(TransactionReviewContract.View view,
                                                                               SaleItemDataStore saleItemDataStore) {
    return new TransactionReviewPresenter(view, saleItemDataStore);
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/login/LoginFragment.java
package com.sendkoin.merchant.ui.login;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.facebook.CallbackManager;
import com.facebook.FacebookCallback;
import com.facebook.FacebookException;
import com.facebook.login.LoginManager;
import com.facebook.login.LoginResult;
import com.sendkoin.api.Merchant;
import com.sendkoin.merchant.KoinMerchantApp;
import com.sendkoin.merchant.R;
import com.sendkoin.merchant.ui.MainActivity;
import com.sendkoin.merchant.ui.orders.list.OrderListFragment;
import com.sendkoin.merchant.ui.login.LoginContract.Presenter;
import com.sendkoin.merchant.ui.onboarding.OnboardingFragment;

import java.util.Collections;

import javax.inject.Inject;

import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import cn.pedant.SweetAlert.SweetAlertDialog;
import timber.log.Timber;

import static android.support.v4.content.ContextCompat.getColor;

public class LoginFragment extends Fragment implements LoginContract.View {
  private static final String TAG = LoginFragment.class.getSimpleName();
  @Inject Presenter presenter;

  private Unbinder unbinder;
  private CallbackManager callbackManager;
  private SweetAlertDialog pDialog;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater,
                           @Nullable ViewGroup container,
                           Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.login_fragment, container, false);
    unbinder = ButterKnife.bind(this, view);
    initFacebookLoginCallback();
    return view;
  }

  private void initFacebookLoginCallback() {
    callbackManager = CallbackManager.Factory.create();
    LoginManager.getInstance().registerCallback(callbackManager,
        new FacebookCallback<LoginResult>() {
          @Override
          public void onSuccess(LoginResult loginResult) {
            presenter.loginWithFacebook(loginResult.getAccessToken().getToken());
          }

          @Override
          public void onCancel() {
            Timber.d("Facebook login cancelled");
          }

          @Override
          public void onError(FacebookException e) {
            Timber.e(e, "Facebook login error!");
          }
        });
  }

  @Override
  public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ((MainActivity) getActivity()).getSupportActionBar().hide();
    setUpDagger();
  }

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    callbackManager.onActivityResult(requestCode, resultCode, data);
  }

  @OnClick(R.id.btn_login_sms)
  void loginWithSms() {
    presenter.loginWithSms();
  }

  @OnClick(R.id.btn_login_fb)
  void loginWithFacebook() {
    LoginManager.getInstance().logInWithReadPermissions(this, Collections.singletonList("email"));
  }


  @Override
  public void onResume() {
    super.onResume();
    presenter.subscribe();
  }

  @Override
  public void onPause() {
    super.onPause();
    presenter.unsubscribe();
  }

  @Override
  public void navigateHome(Merchant merchant) {
    FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
    if (merchant != null && isOnboardingComplete(merchant)) {
      fragmentTransaction.replace(R.id.fragment_placeholder, new OrderListFragment());
    } else {
      fragmentTransaction.replace(
          R.id.fragment_placeholder, new OnboardingFragment(), OnboardingFragment.TAG);
    }

    fragmentTransaction.commit();
  }

  private boolean isOnboardingComplete(Merchant merchant) {
    return !TextUtils.isEmpty(merchant.store_name)
        && !TextUtils.isEmpty(merchant.store_location)
        && !TextUtils.isEmpty(merchant.store_type);
  }

  @Override
  public void showLoginLoading() {
    pDialog = new SweetAlertDialog(getContext(), SweetAlertDialog.PROGRESS_TYPE);
    pDialog.getProgressHelper().setBarColor(getColor(getContext(), R.color.colorAccent));
    pDialog.setTitleText(getString(R.string.login_loading_message));
    pDialog.setCancelable(false);
    pDialog.show();
  }

  @Override
  public void showLoginSuccessful() {
    pDialog.dismissWithAnimation();
  }

  @Override
  public void showLoginFailed(Throwable e) {
    pDialog.setTitleText("Login failed");
    pDialog.changeAlertType(SweetAlertDialog.ERROR_TYPE);
  }

  @Override
  public void onDestroyView() {
    super.onDestroyView();
    unbinder.unbind();
  }

  private void setUpDagger() {
    DaggerLoginScreenComponent.builder()
        .netComponent(((KoinMerchantApp) getActivity()
            .getApplicationContext())
            .getNetComponent())
        .loginScreenModule(new LoginScreenModule(this))
        .build()
        .inject(this);
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ContextProvider.java
package com.sendkoin.merchant;

import android.content.Context;

interface ContextProvider {
  Context getActivityContext();
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/data/transactions/TransactionRepository.java
package com.sendkoin.merchant.data.transactions;

import com.pushtorefresh.storio.sqlite.operations.delete.DeleteResult;
import com.pushtorefresh.storio.sqlite.operations.put.PutResults;
import com.sendkoin.api.ListTransactionsRequest;
import com.sendkoin.api.ListTransactionsResponse;
import com.sendkoin.api.QueryParameters;
import com.sendkoin.api.Transaction;
import com.sendkoin.sql.entities.KoinTransactionEntity;

import java.util.List;

import javax.inject.Inject;

import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;

public class TransactionRepository {
  private static final int MAX_TRANSACTIONS = 3000;
  private final TransactionDataStore transactionDataStore;
  private final TransactionService transactionService;
  private Integer pageNumber = 1;

  @Inject
  public TransactionRepository(TransactionDataStore transactionDataStore,
                               TransactionService transactionService) {
    this.transactionDataStore = transactionDataStore;
    this.transactionService = transactionService;
  }

  /**
   * 1. Load transactions with a limit of MAX_TRANSACTIONS + 1. We fetch 1 more than
   * MAX_TRANSACTIONS to see if we have more than our limit in the database.
   * <p>
   * 2. If the size of our results is greater than MAX_TRANSACTIONS,
   * we delete (MAX_TRANSACTIONS / 2) old transactions from the local database.
   */
  public Observable<List<KoinTransactionEntity>> loadTransactionsFromLocal() {
    return transactionDataStore.getAllTransactions(MAX_TRANSACTIONS + 1)
        .subscribeOn(Schedulers.io())
        .map(transactionEntities -> {
          if (transactionEntities.size() > MAX_TRANSACTIONS) {
            Long olderThan = transactionEntities.get(MAX_TRANSACTIONS / 2).getCreatedAt();
            deleteTransactionsOlderThan(olderThan);
          }

          return transactionEntities;
        })
        .observeOn(AndroidSchedulers.mainThread());
  }

  private Observable<DeleteResult> deleteTransactionsOlderThan(Long olderThan) {
    return transactionDataStore.deleteTransactionsOlderThan(olderThan)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());
  }

  /**
   * Load the latest unseen transactions from the server and save to the local database.
   * It needs to fetch in ASCENDING order in case it doesn't finish loading from the server and
   * then getLatestTransaction() will return the latest and the missing results won't be fetched.
   * <p>
   * 1. Fetch latest orderEntity from the local database.
   * 2. Query server for all transactions newer than that.
   * 3. Save results returned by server in the local database.
   *
   * @param queryParameters
   */
  public Observable<PutResults<KoinTransactionEntity>> loadNewTransactionsFromServer(
      QueryParameters queryParameters) {
    return transactionDataStore.getLatestTransaction()
        .subscribeOn(Schedulers.io())
        .take(1)
        .flatMap(latestTransactionEntity -> {
          long updatesAfter =
              latestTransactionEntity != null ? latestTransactionEntity.getCreatedAt() : 0;
          ListTransactionsRequest listTransactionsRequest = new ListTransactionsRequest.Builder()
              .query_parameters(queryParameters.newBuilder().updates_after(updatesAfter).build())
              .build();
          return recursivelyLoadTransactionsFromServer(listTransactionsRequest, 1);
        })
        .flatMap(transactionDataStore::createOrUpdate)
        .observeOn(AndroidSchedulers.mainThread());
  }

  public Observable<ListTransactionsResponse> loadOldTransactionsFromServer(
      QueryParameters queryParameters) {
    return transactionDataStore.getEarliestTransaction()
        .subscribeOn(Schedulers.io())
        .take(1)
        .flatMap(earliestTransactionEntity -> {
          long updatesBefore =
              earliestTransactionEntity != null ? earliestTransactionEntity.getCreatedAt() : 0;
          ListTransactionsRequest listTransactionsRequest = new ListTransactionsRequest.Builder()
              .query_parameters(queryParameters.newBuilder().updates_before(updatesBefore).build())
              .build();

          return transactionService.listTransactions(listTransactionsRequest, pageNumber++);
        })
        .observeOn(AndroidSchedulers.mainThread());
  }

  /**
   * Fetches the transactions from the server and if `has_next_page` is true, recursively is called
   * again until `has_next_page` is false.
   * <p>
   * Using `concatWith` to combine all the results of the
   * recursive calls into one observable.
   * <p>
   * ConcatMap ensures that there is no interleaving between mutliple observables from the multiple
   * recursive calls.
   *
   * @param listTransactionsRequest The request defining our server query.
   * @param page                    The page number of the results returned by the server.
   * @return An observable consisting of the combined transactions from the pages.
   */
  private Observable<List<Transaction>> recursivelyLoadTransactionsFromServer(
      ListTransactionsRequest listTransactionsRequest, int page) {
    return transactionService.listTransactions(listTransactionsRequest, page)
        .subscribeOn(Schedulers.io())
        .concatMap(listTransactionsResponse -> {
          if (listTransactionsResponse.has_next_page) {
            return Observable.just(listTransactionsResponse.transactions).concatWith(
                recursivelyLoadTransactionsFromServer(listTransactionsRequest, page + 1));
          } else {
            return Observable.just(listTransactionsResponse.transactions);
          }
        });
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/MainActivity.java
package com.sendkoin.merchant.ui;

import android.content.Context;
import android.os.Bundle;
import android.support.design.widget.NavigationView;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import com.sendkoin.merchant.KoinMerchantApp;
import com.sendkoin.merchant.R;
import com.sendkoin.merchant.dagger.component.NetComponent;
import com.sendkoin.merchant.data.authentication.SessionManager;
import com.sendkoin.merchant.ui.home.HomeFragment;
import com.sendkoin.merchant.ui.login.LoginFragment;
import com.sendkoin.merchant.ui.onboarding.OnboardingFragment;
import com.sendkoin.merchant.ui.transactions.history.TransactionHistoryFragment;

import butterknife.BindView;
import butterknife.ButterKnife;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper;

public class MainActivity extends AppCompatActivity {
  @BindView(R.id.toolbar) Toolbar toolbar;
  @BindView(R.id.drawer_layout) DrawerLayout drawer;
  @BindView(R.id.nav_view) NavigationView navigationView;

  private ActionBarDrawerToggle drawerToggle;
  private CompositeSubscription compositeSubscription = new CompositeSubscription();

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
    setUpToolbar();

    if (savedInstanceState == null) {
      navigationView.setNavigationItemSelectedListener(this::onNavigationItemSelected);
      loadMerchant();
      String sessionToken = sessionManager().getSessionToken();
      Fragment fragment = sessionToken != null ? new HomeFragment() : new LoginFragment();
      getSupportFragmentManager()
          .beginTransaction()
          .replace(R.id.fragment_placeholder, fragment)
          .commit();
    }
  }

  private void loadMerchant() {
    Subscription subscription = getNetComponent().merchantDataStore().getOnly()
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(merchant -> {
          View headerView = navigationView.getHeaderView(0);
          TextView merchantName = (TextView) headerView.findViewById(R.id.tv_drawer_merchant_name);
          TextView merchantAddress = (TextView) headerView.findViewById(R.id.tv_drawer_merchant_location);
          merchantName.setText(merchant.get().getStoreName());
          merchantAddress.setText(merchant.get().getStoreLocation());
        });

    compositeSubscription.add(subscription);
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    compositeSubscription.unsubscribe();
  }

  private SessionManager sessionManager() {
    return getNetComponent().sessionManager();
  }

  private NetComponent getNetComponent() {
    return ((KoinMerchantApp) getApplication()).getNetComponent();
  }

  @Override
  protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
  }

  /* TODO(Zen): Implement. */
  private void setUpToolbar() {
    setSupportActionBar(toolbar);
    drawerToggle = new ActionBarDrawerToggle(this, drawer, toolbar,
        R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawer.addDrawerListener(drawerToggle);
    drawerToggle.syncState();
  }

  /* Auto-generated. TODO(Zen): Implement. */
  public void onBackPressed() {
    if (drawer.isDrawerOpen(GravityCompat.START)) {
      drawer.closeDrawer(GravityCompat.START);
    }

    // Delegate back presses for onboarding fragment to the fragment's back press handler in order
    // to allow it to navigate to a previous view in the view pager. If the back button is pressed
    // when at the first view of the view pager, then call super.onBackPressed().
    OnboardingFragment onboardingFragment =
        (OnboardingFragment) getSupportFragmentManager().findFragmentByTag(OnboardingFragment.TAG);

    if (onboardingFragment != null) {
      if (onboardingFragment.canGoBack()) {
        onboardingFragment.onBackPressed();
      } else {
        super.onBackPressed();
      }
    } else {
      super.onBackPressed();
    }
  }


  public boolean onNavigationItemSelected(MenuItem item) {
    // Handle navigation view item clicks here.
    int id = item.getItemId();
    Fragment targetFragment = new HomeFragment();

    if (id == R.id.nav_history) {
      targetFragment = new TransactionHistoryFragment();
    } else if (id == R.id.nav_profile) {
      Toast.makeText(this, "Unsupported operation", Toast.LENGTH_SHORT).show();
    } else if (id == R.id.nav_share) {
      Toast.makeText(this, "Unsupported operation", Toast.LENGTH_SHORT).show();
    }

    FragmentTransaction fragmentTransaction = getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.fragment_placeholder, targetFragment)
        .addToBackStack(null);
    drawer.closeDrawer(GravityCompat.START);
    fragmentTransaction.commit();
    return true;
  }

  public ActionBarDrawerToggle getDrawerToggle() {
    return drawerToggle;
  }

  public void navigateToLoginScreen() {
    getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.fragment_placeholder, new LoginFragment())
        .commit();
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/data/sql/KoinSQLiteOpenHelper.java
package com.sendkoin.merchant.data.sql;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import com.sendkoin.sql.tables.KoinTransactionTable;
import com.sendkoin.sql.tables.MerchantTable;
import com.sendkoin.sql.tables.OrderItemTable;
import com.sendkoin.sql.tables.KoinOrderTable;
import com.sendkoin.sql.tables.SaleItemTable;

public class KoinSQLiteOpenHelper extends SQLiteOpenHelper {
  public static final int VERSION = 6;
  public static final String DATABASE_NAME = "koin_merchant_db";

  public KoinSQLiteOpenHelper(Context context) {
    super(context, DATABASE_NAME, null, VERSION);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL(MerchantTable.getCreateTableQuery());
    db.execSQL(SaleItemTable.getCreateTableQuery());
    db.execSQL(KoinTransactionTable.getCreateTableQuery());
    db.execSQL(KoinOrderTable.getCreateTableQuery());
    db.execSQL(OrderItemTable.getCreateTableQuery());
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/orders/details/OrderDetailsListAdapter.java
package com.sendkoin.merchant.ui.orders.details;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.sendkoin.api.SaleItem;
import com.sendkoin.merchant.R;

import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

class OrderDetailsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
  public static final String DEFAULT_ITEM_NAME = "One Time Payment";
  private List<SaleItem> saleItems;

  public void setOrderDetailsList(List<SaleItem> saleItems) {
    this.saleItems = saleItems;
    notifyDataSetChanged();
  }

  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
    View inflatedView = layoutInflater.inflate(R.layout.order_details_list_item, parent, false);
    return new OrderDetailsViewHolder(inflatedView);
  }

  @Override
  public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    SaleItem saleItem = saleItems.get(position);
    ((OrderDetailsViewHolder) holder).itemName.setText(saleItem.name != null
        ? saleItem.name
        : DEFAULT_ITEM_NAME);
    ((OrderDetailsViewHolder) holder).quantity.setText(String.valueOf(saleItem.quantity));
    if (saleItem.customer_notes != null) {
      ((OrderDetailsViewHolder) holder).customerNotesTitle.setVisibility(View.VISIBLE);
      ((OrderDetailsViewHolder) holder).customerNotesTitle.setText(R.string.customer_notes_title);
      ((OrderDetailsViewHolder) holder).customerNotes.setVisibility(View.VISIBLE);
      ((OrderDetailsViewHolder) holder).customerNotes.setText(saleItem.customer_notes);
    }
  }

  @Override
  public int getItemCount() {
    return saleItems != null ? saleItems.size() : 0;
  }

  class OrderDetailsViewHolder extends RecyclerView.ViewHolder {
    @BindView(R.id.tv_order_details_item_name) TextView itemName;
    @BindView(R.id.tv_order_details_quantity) TextView quantity;
    @BindView(R.id.tv_order_details_customer_notes) TextView customerNotes;
    @BindView(R.id.tv_order_details_customer_notes_title) TextView customerNotesTitle;

    OrderDetailsViewHolder(View itemView) {
      super(itemView);
      ButterKnife.bind(this, itemView);
    }
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/verify/VerifyOrderModule.java
package com.sendkoin.merchant.ui.transactions.verify;

import com.sendkoin.merchant.data.orders.OrderService;
import com.sendkoin.merchant.ui.CustomScope;
import com.sendkoin.merchant.ui.transactions.verify.VerifyOrderContract.Presenter;

import dagger.Module;
import dagger.Provides;
import retrofit2.Retrofit;

@Module
public class VerifyOrderModule {
  private final VerifyOrderContract.View view;

  public VerifyOrderModule(VerifyOrderContract.View view) {
    this.view = view;
  }

  @Provides
  @CustomScope
  VerifyOrderContract.View provideVerifyTransactionView() {
    return view;
  }

  @Provides
  @CustomScope
  public OrderService orderService(Retrofit retrofit) {
    return retrofit.create(OrderService.class);
  }

  @Provides
  @CustomScope
  Presenter provideVerifyTransactionPresenter(VerifyOrderContract.View view,
                                              OrderService orderService) {
    return new VerifyOrderPreseneter(view, orderService);
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/onboarding/OnboardingComponent.java
package com.sendkoin.merchant.ui.onboarding;

import com.sendkoin.merchant.dagger.component.NetComponent;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Component;

@CustomScope
@Component(dependencies = NetComponent.class, modules = OnboardingModule.class)
public interface OnboardingComponent {
  void inject(OnboardingFragment onboardingFragment);
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/verify/VerifyOrderContract.java
package com.sendkoin.merchant.ui.transactions.verify;

import com.sendkoin.api.PickUpOrderResponse;
import com.sendkoin.api.Transaction;
import com.sendkoin.merchant.ui.BasePresenter;

public interface VerifyOrderContract {
  interface View {
    void showSuccessful(PickUpOrderResponse pickUpOrderResponse);

    void showPreviouslyVerified(Transaction transaction);

    void showFailed(Transaction transaction);

    void startCamera();

    void stopCamera();
  }

  interface Presenter extends BasePresenter {
    void verifyOrder(String transactionToken);
  }
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/transactions/history/TransactionHistoryComponent.java
package com.sendkoin.merchant.ui.transactions.history;

import com.sendkoin.merchant.dagger.component.NetComponent;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Component;

@Component(dependencies = NetComponent.class, modules = TransactionHistoryModule.class)
@CustomScope
public interface TransactionHistoryComponent {
  void inject(TransactionHistoryFragment transactionHistoryFragment);
}
<file_sep>/app/src/main/java/com/sendkoin/merchant/ui/currentsale/CurrentSaleComponent.java
package com.sendkoin.merchant.ui.currentsale;

import com.sendkoin.merchant.dagger.component.NetComponent;
import com.sendkoin.merchant.ui.CustomScope;

import dagger.Component;

@CustomScope
@Component(dependencies = NetComponent.class, modules = CurrentSaleModule.class)
public interface CurrentSaleComponent {
  void inject(CurrentSaleFragment fragment);
}
