Commit d99b8b82 by Tuukka Kivilahti

Merge branch 'feature/checkout-v2' into 'master'

Checkout v2 api

See merge request !435
2 parents 762068cf 5bfca6b4
Showing with 1367 additions and 13 deletions
/*
* Copyright Codecrew Ry
*
* All rights reserved.
*
* This license applies to any software containing a notice placed by the
* copyright holder. Such software is herein referred to as the Software.
* This license covers modification, distribution and use of the Software.
*
* Any distribution and use in source and binary forms, with or without
* modification is not permitted without explicit written permission from the
* copyright owner.
*
* A non-exclusive royalty-free right is granted to the copyright owner of the
* Software to use, modify and distribute all modifications to the Software in
* future versions of the Software.
*
*/
package fi.codecrew.moya.beans;
import fi.codecrew.moya.beans.checkout.CheckoutCreateResponsePojo;
import fi.codecrew.moya.model.Bill;
import fi.codecrew.moya.util.CheckoutBank;
import fi.codecrew.moya.util.CheckoutReturnType;
import javax.ejb.Local;
import java.util.List;
import java.util.Map;
@Local
public interface CheckoutFiV2BeanLocal {
public CheckoutCreateResponsePojo createCheckoutBill(Bill bill);
boolean verifyResponse(Map<String, String> fields);
boolean isPaymentEnabled();
}
package fi.codecrew.moya.beans.checkout;
public class CheckoutCreateResponseFormField {
private String name;
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
package fi.codecrew.moya.beans.checkout;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class CheckoutCreateResponsePojo {
private String transactionId;
private String href;
private List<CheckoutCreateResponseProviderPojo> providers;
public String getTransactionId() {
return transactionId;
}
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
public String getHref() {
return href;
}
public void setHref(String href) {
this.href = href;
}
public List<CheckoutCreateResponseProviderPojo> getProvidersByGroup(String... groups) {
Set<String> groupSet = new HashSet<>(Arrays.asList(groups));
return providers.stream().filter(p -> groupSet
.contains(p.getGroup()))
.sorted(Comparator.comparing(CheckoutCreateResponseProviderPojo::getName))
.collect(Collectors.toList());
}
public List<CheckoutCreateResponseProviderPojo> getProviders() {
return providers;
}
public void setProviders(List<CheckoutCreateResponseProviderPojo> providers) {
this.providers = providers;
}
}
package fi.codecrew.moya.beans.checkout;
import java.util.List;
public class CheckoutCreateResponseProviderPojo {
private String url;
private String icon;
private String svg;
private String group;
private String name;
private String id;
private List<CheckoutCreateResponseFormField> parameters;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getSvg() {
return svg;
}
public void setSvg(String svg) {
this.svg = svg;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<CheckoutCreateResponseFormField> getParameters() {
return parameters;
}
public void setParameters(List<CheckoutCreateResponseFormField> parameters) {
this.parameters = parameters;
}
}
...@@ -375,10 +375,12 @@ public class BillBean implements BillBeanLocal { ...@@ -375,10 +375,12 @@ public class BillBean implements BillBeanLocal {
bill.setBillLines(new ArrayList<BillLine>()); bill.setBillLines(new ArrayList<BillLine>());
} }
bill.getBillLines().add(new BillLine(bill, product, count, foodwave, additionalDescription)); BillLine productLine = new BillLine(bill, product, count, foodwave, additionalDescription);
bill.getBillLines().add(productLine);
BigDecimal discountPrice = product.getPrice(); BigDecimal discountPrice = product.getPrice();
for (Discount disc : discountBean.getActiveDiscountsByProduct(product, count, bill.getSentDate(), bill.getUser())) { for (Discount disc : discountBean.getActiveDiscountsByProduct(product, count, bill.getSentDate(), bill.getUser())) {
BillLine line = new BillLine(bill, product, disc,discountPrice, count); BillLine line = new BillLine(bill, product, disc, discountPrice, count);
line.setDiscountSource(productLine);
// unitPrice is negative, so add. Do not subtract // unitPrice is negative, so add. Do not subtract
discountPrice = discountPrice.add(line.getUnitPrice()); discountPrice = discountPrice.add(line.getUnitPrice());
bill.getBillLines().add(line); bill.getBillLines().add(line);
......
...@@ -665,6 +665,11 @@ public class BootstrapBean implements BootstrapBeanLocal { ...@@ -665,6 +665,11 @@ public class BootstrapBean implements BootstrapBeanLocal {
"ALTER TABLE food_wave_templates DROP COLUMN max_foods;" "ALTER TABLE food_wave_templates DROP COLUMN max_foods;"
}); });
dbUpdates.add(new String[]{
"ALTER TABLE bill_lines ADD COLUMN discount_source_id INTEGER",
"ALTER TABLE bill_lines ADD CONSTRAINT FK_bill_lines_discount_source_id FOREIGN KEY (discount_source_id) REFERENCES bill_lines (id)"
});
} }
......
/*
* Copyright Codecrew Ry
*
* All rights reserved.
*
* This license applies to any software containing a notice placed by the
* copyright holder. Such software is herein referred to as the Software.
* This license covers modification, distribution and use of the Software.
*
* Any distribution and use in source and binary forms, with or without
* modification is not permitted without explicit written permission from the
* copyright owner.
*
* A non-exclusive royalty-free right is granted to the copyright owner of the
* Software to use, modify and distribute all modifications to the Software in
* future versions of the Software.
*
*/
package fi.codecrew.moya.beans;
import com.google.gson.Gson;
import fi.codecrew.moya.beans.checkout.CheckoutCreateQueryBuilder;
import fi.codecrew.moya.beans.checkout.CheckoutCreateResponsePojo;
import fi.codecrew.moya.beans.checkout.CheckoutCredentials;
import fi.codecrew.moya.beans.checkout.CheckoutQueryBuilder;
import fi.codecrew.moya.enums.apps.BillPermission;
import fi.codecrew.moya.facade.BillFacade;
import fi.codecrew.moya.model.Bill;
import fi.codecrew.moya.model.LanEventPrivateProperty;
import fi.codecrew.moya.model.LanEventPrivatePropertyKey;
import fi.codecrew.moya.utilities.moyamessage.MoyaEventType;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.security.RolesAllowed;
import javax.ejb.EJB;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Session Bean implementation class CheckoutFiBean
*/
@Stateless
@LocalBean
public class CheckoutFiV2Bean implements CheckoutFiV2BeanLocal {
public CheckoutFiV2Bean() {
}
@EJB
private EventBean eventbean;
@EJB
private PermissionBeanLocal permbean;
@EJB
private LoggingBeanLocal logbean;
@EJB
private BillFacade billfacade;
@EJB
private BillPBean billpbean;
private static final Logger logger = LoggerFactory.getLogger(CheckoutFiBean.class);
static final BigDecimal TO_CENTS = BigDecimal.valueOf(100);
static final Set<String> DISCARD_BANKS = new HashSet<String>(Arrays.asList("ape", "tilisiirto", "neopay"));
public boolean isPaymentEnabled() {
final LanEventPrivateProperty expire = eventbean.getPrivateProperty(LanEventPrivatePropertyKey.CHECKOUT_FI_KEY_EXPIRE);
final String merchantid = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_ID);
final String merchantPassword = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_PASSWORD);
final boolean expired = (expire == null || expire.getDateValue() == null || new Date().after(expire.getDateValue()));
boolean ret = !expired
&& merchantid != null && !merchantid.isEmpty()
&& merchantPassword != null && !merchantPassword.isEmpty();
return ret;
}
protected CheckoutCredentials getCredentials() {
final LanEventPrivateProperty expire = eventbean.getPrivateProperty(LanEventPrivatePropertyKey.CHECKOUT_FI_KEY_EXPIRE);
final String merchantid = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_ID);
final String merchantPassword = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_PASSWORD);
Date date = null;
if (expire != null) {
date = expire.getDateValue();
}
CheckoutCredentials ret = new CheckoutCredentials(date, merchantid, merchantPassword);
return ret;
}
private static final String STAMP_FIELD = "checkout-stamp";
private static final String STATUS_FIELD = "checkout-status";
@Override
public boolean verifyResponse(Map<String, String> fields) {
String checksum = CheckoutQueryBuilder.generatesignature(getCredentials().getMerchantPassword(), fields, "");
String signature = fields.get(CheckoutQueryBuilder.SIGNATURE_HEADER_NAME);
if (checksum != null && signature != null && signature.toLowerCase().equals(checksum.toLowerCase())) {
if (!fields.containsKey(STATUS_FIELD) || !fields.containsKey(STAMP_FIELD)) {
logBanking(MoyaEventType.BANKING_ERROR, "Missing fields in checkout response, although signature ok", fields);
return false;
}
final String stamp = fields.get(STAMP_FIELD);
final String status = fields.get(STATUS_FIELD);
final String[] splitStamp = stamp.split(CheckoutQueryBuilder.STAMP_SPLITCHAR);
Bill bill = billfacade.find(Integer.parseInt(splitStamp[0]));
switch (status) {
case "ok":
if (bill.getAccountEvent() == null
&& bill.getPaidDate() == null) {
logger.info("Trying to mark bill {} paid", bill);
billpbean.markPaid(bill, Calendar.getInstance(), false, Bill.BillPaymentSource.CHECKOUT);
logBanking(MoyaEventType.BANKING_MESSAGE, "Marking bill paid from checkout v2. Received bill status " + bill, fields);
} else {
logBanking(MoyaEventType.BANKING_MESSAGE, "Bill already marked paid: " + bill, fields);
}
return true;
case "fail":
case "pending":
case "delayed":
logBanking(MoyaEventType.BANKING_MESSAGE, "Received status: " + status, fields);
break;
default:
logBanking(MoyaEventType.BANKING_ERROR, "Unknown status message " + status, fields);
}
} else {
logBanking(MoyaEventType.BANKING_ERROR, "Unable to verify checkout signature: ", fields);
}
return false;
}
private void logBanking(MoyaEventType type, String msg, Map<String, String> fields) {
logbean.sendMessage(type, permbean.getCurrentUser(), msg, fields.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&")));
}
@Override
@RolesAllowed(BillPermission.S_CREATE_VERKKOMAKSU)
public CheckoutCreateResponsePojo createCheckoutBill(Bill bill) {
if (bill.isFoowavePaymentOver()) {
return null;
}
CheckoutCreateQueryBuilder queryBuilder = new CheckoutCreateQueryBuilder(getCredentials());
if (!queryBuilder.isCredentialsValid()) {
return null;
}
queryBuilder.createQuery(bill, eventbean.getCurrentHostname());
CheckoutCreateResponsePojo responsePojo = null;
try (CloseableHttpResponse response = queryBuilder.sendQuery();
InputStream queryContent = response.getEntity().getContent()) {
String responseText = IOUtils.toString(queryContent, StandardCharsets.UTF_8.toString());
try {
responsePojo = new Gson().fromJson(responseText, CheckoutCreateResponsePojo.class);
} catch (Exception e) {
logbean.sendMessage(MoyaEventType.BANKING_MESSAGE, permbean.getCurrentUser(), "Invalid response from checkout: " + e.getMessage() + "'" + responseText + "'");
logger.warn("Unable to parse checkout respose: " + responsePojo, e);
}
} catch (IOException e) {
logger.warn("Error sending checkout.fi request ", e);
logbean.sendMessage(MoyaEventType.BANKING_MESSAGE, permbean.getCurrentUser(), "IoError while sending creating payment to checkout: " + e.getMessage());
}
return responsePojo;
}
}
package fi.codecrew.moya.beans.checkout;
import com.google.gson.Gson;
import fi.codecrew.moya.model.Bill;
import fi.codecrew.moya.model.BillLine;
import fi.codecrew.moya.model.EventUser;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class CheckoutCreateQueryBuilder extends CheckoutQueryBuilder {
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
public CheckoutCreateQueryBuilder(CheckoutCredentials credentials) {
super(credentials);
}
public void createQuery(Bill bill, String callbackHostname) {
final String returnUrl = new StringBuilder()
.append("https://")
.append(callbackHostname)
.append("/MoyaWeb/checkout/")
.toString();
CheckoutV2CreatePojo createQuery = new CheckoutV2CreatePojo();
createQuery.setStamp(getStamp(bill));
createQuery.setReference(bill.getReferenceNumber().toString());
createQuery.setAmount(bill.totalPrice().multiply(TO_CENTS).intValue());
CheckoutV2CallbackUrlPojo callbackUrls = new CheckoutV2CallbackUrlPojo(returnUrl + "v2success.jsf", returnUrl + "v2cancel.jsf");
createQuery.setRedirectUrls(callbackUrls);
createQuery.setCallbackUrls(callbackUrls);
createQuery.setCustomer(creatCustomer(bill));
//queryBuilder.addParam(CheckoutFiPaymentParam.MERCHANT, queryBuilder.getMerchantId());
//queryBuilder.addParam(CheckoutFiPaymentParam.DELIVERY_DATE, new SimpleDateFormat(DATEFORMAT).format(new Date()));
// The api does not allow negative values for a line.
Map<BillLine, BigDecimal> discountedPrice = new HashMap<>();
for (BillLine line : bill.getBillLines()) {
discountedPrice.computeIfAbsent(line.getDiscountSource() == null ? line : line.getDiscountSource(), l -> l.getUnitPrice());
if (line.getDiscountSource() == null) {
continue;
}
discountedPrice.compute(line.getDiscountSource(), (k, v) -> {
if (line.getQuantity().compareTo(k.getQuantity()) != 0) {
throw new RuntimeException("Product quantity does not match for the discount " + k + ": discount" + line);
}
return v.add(line.getUnitPrice());
});
}
createQuery.setItems(discountedPrice.entrySet().stream().map(l -> createBillLine(l.getKey(), l.getValue())).collect(Collectors.toList()));
String createJson = new Gson().toJson(createQuery);
this.setContent(createJson);
}
private CheckoutV2CustomerPojo creatCustomer(Bill bill) {
CheckoutV2CustomerPojo cust = new CheckoutV2CustomerPojo();
EventUser user = bill.getUser();
cust.setEmail(user.getEmail());
cust.setFirstName(user.getFirstnames());
cust.setLastName(user.getLastname());
cust.setPhone(user.getPhone());
//cust.setVatId(user.getV);
return cust;
}
private static CheckoutV2ItemPojo createBillLine(BillLine line, BigDecimal discountedValue) {
CheckoutV2ItemPojo item = new CheckoutV2ItemPojo();
item.setUnitPrice(discountedValue.multiply(TO_CENTS).intValue());
item.setUnits(line.getQuantity().intValue());
item.setVatPercentage(line.getVat().multiply(HUNDRED).intValue());
item.setProductCode(Integer.toString(line.getId()));
item.setDeliveryDate(ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
return item;
}
private static String getStamp(Bill bill) {
final StringBuilder stamp = new StringBuilder();
stamp.append(bill.getId());
stamp.append(STAMP_SPLITCHAR);
stamp.append(System.currentTimeMillis() / 1000);
return stamp.toString();
}
@Override
public String getUri() {
return "/payments";
}
@Override
protected CheckoutQueryBuilder.Method getMethod() {
return Method.POST;
}
}
package fi.codecrew.moya.beans.checkout;
import java.util.Date;
public class CheckoutCredentials {
private final String merchantPassword;
private final Date expireDate;
private final String merchantId;
public CheckoutCredentials(Date expire, String merchantId, String merchantPassword) {
this.expireDate = expire;
this.merchantId = merchantId;
this.merchantPassword = merchantPassword;
}
public String getMerchantPassword() {
return merchantPassword;
}
public Date getExpireDate() {
return expireDate;
}
public String getMerchantId() {
return merchantId;
}
}
package fi.codecrew.moya.beans.checkout;
import fi.codecrew.moya.checkoutfi.CheckoutQueryParam;
import org.apache.commons.codec.binary.Hex;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public abstract class CheckoutQueryBuilder {
public static final BigDecimal TO_CENTS = BigDecimal.valueOf(100);
public static final String STAMP_SPLITCHAR = "b";
private static final String REMOTE_URL = "https://api.checkout.fi";
public static final String SIGNATURE_HEADER_NAME = "signature";
private final Map<CheckoutQueryParam, String> values = new HashMap<>();
private final Map<String, String> headers = new HashMap<>();
private final String nonce = UUID.randomUUID().toString();
private final CheckoutCredentials credentials;
private String content;
private static final Logger logger = LoggerFactory.getLogger(CheckoutQueryBuilder.class);
public Map<String, String> getHeaders() {
return headers;
}
public enum Method {
POST, GET
}
public CheckoutQueryBuilder(CheckoutCredentials credentials) {
this.credentials = credentials;
resetHeaders();
}
private void resetHeaders() {
headers.put("checkout-account", credentials.getMerchantId());
headers.put("checkout-algorithm", HASHING_ALGO.toLowerCase());
headers.put("checkout-nonce", nonce);
headers.put("checkout-method", getMethod().toString());
headers.put("checkout-timestamp", ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT));
}
public abstract String getUri();
protected abstract Method getMethod();
public boolean isCredentialsValid() {
return credentials.getExpireDate() != null && new Date().before(credentials.getExpireDate())
&& credentials.getMerchantId() != null && !credentials.getMerchantId().isEmpty()
&& credentials.getMerchantPassword() != null && !credentials.getMerchantPassword().isEmpty();
}
private static final String HASHING_ALGO = "SHA256";
private static final String HMAC_ALGO = "Hmac" + HASHING_ALGO;
public static String generatesignature(String hmacKey, Map<String, String> headers, String requestBody) {
StringBuilder hashString = new StringBuilder();
headers.entrySet().stream()
.filter(e -> e.getKey().startsWith("checkout-"))
.map(e -> e.getKey() + ":" + e.getValue() + "\n")
.sorted()
.forEach(hashString::append);
if (requestBody != null && !requestBody.isEmpty()) {
hashString.append(requestBody);
}
return calculateHmac(hmacKey, hashString.toString());
}
public static String calculateHmac(String key, String text) {
try {
Mac mac = Mac.getInstance(HMAC_ALGO);
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), HMAC_ALGO);
mac.init(secret_key);
String hash = Hex.encodeHexString(mac.doFinal(text.getBytes(StandardCharsets.UTF_8)));
logger.info("Generated signature {} from {}", hash, text);
return hash;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
logger.warn("Unable to create hmac algo", e);
}
return null;
}
public void setContent(String json) {
this.content = json;
}
public String getContent() {
return content;
}
public CloseableHttpResponse sendQuery() throws ClientProtocolException, IOException {
String signature = generatesignature(credentials.getMerchantPassword(), headers, content);
headers.put(SIGNATURE_HEADER_NAME, signature);
CloseableHttpResponse response = null;
String url = REMOTE_URL + this.getUri();
final HttpPost postRequest = new HttpPost(url);
postRequest.setEntity(new StringEntity(content, ContentType.APPLICATION_JSON));
this.getHeaders().forEach(postRequest::setHeader);
HttpClientBuilder cliBuilder = HttpClientBuilder.create();
CloseableHttpClient cli = cliBuilder.build();
response = cli.execute(postRequest);
return response;
}
}
package fi.codecrew.moya.beans.checkout;
public class CheckoutV2AddressPojo {
private String streetAddress;
private String postalCode;
private String city;
private String county;
private String country;
public String getStreetAddress() {
return streetAddress;
}
public void setStreetAddress(String streetAddress) {
this.streetAddress = streetAddress;
}
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCounty() {
return county;
}
public void setCounty(String county) {
this.county = county;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
if (country.length() != 2) {
throw new IllegalArgumentException("Country length must be 2: " + country);
}
this.country = country;
}
}
package fi.codecrew.moya.beans.checkout;
public class CheckoutV2CallbackUrlPojo {
private String success;
private String cancel;
public CheckoutV2CallbackUrlPojo() {
super();
}
public CheckoutV2CallbackUrlPojo(String success, String cancel) {
this();
this.success = success;
this.cancel = cancel;
}
public String getSuccess() {
return success;
}
public void setSuccess(String success) {
this.success = success;
}
public String getCancel() {
return cancel;
}
public void setCancel(String cancel) {
this.cancel = cancel;
}
}
package fi.codecrew.moya.beans.checkout;
import fi.codecrew.moya.checkoutfi.CheckoutFiPaymentParam;
import fi.codecrew.moya.model.Bill;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class CheckoutV2CreatePojo {
private String stamp;
private String reference;
private Integer amount;
private String currency = "EUR";
private String language = "FI"; // SV, EN
private String orderId;
private List<CheckoutV2ItemPojo> items = new ArrayList<>();
private CheckoutV2CustomerPojo customer;
private CheckoutV2AddressPojo deliveryAddress;
// Required
private CheckoutV2CallbackUrlPojo redirectUrls;
private CheckoutV2CallbackUrlPojo callbackUrls;
private int callbackDelay = 0;
public CheckoutV2CreatePojo() {
super();
}
public String getStamp() {
return stamp;
}
public void setStamp(String stamp) {
this.stamp = stamp;
}
public String getReference() {
return reference;
}
public void setReference(String reference) {
this.reference = reference;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public List<CheckoutV2ItemPojo> getItems() {
return items;
}
public void setItems(List<CheckoutV2ItemPojo> items) {
this.items = items;
}
public CheckoutV2CustomerPojo getCustomer() {
return customer;
}
public void setCustomer(CheckoutV2CustomerPojo customer) {
this.customer = customer;
}
public CheckoutV2AddressPojo getDeliveryAddress() {
return deliveryAddress;
}
public void setDeliveryAddress(CheckoutV2AddressPojo deliveryAddress) {
this.deliveryAddress = deliveryAddress;
}
public CheckoutV2CallbackUrlPojo getRedirectUrls() {
return redirectUrls;
}
public void setRedirectUrls(CheckoutV2CallbackUrlPojo redirectUrls) {
this.redirectUrls = redirectUrls;
}
public CheckoutV2CallbackUrlPojo getCallbackUrls() {
return callbackUrls;
}
public void setCallbackUrls(CheckoutV2CallbackUrlPojo callbackUrls) {
this.callbackUrls = callbackUrls;
}
public int getCallbackDelay() {
return callbackDelay;
}
public void setCallbackDelay(int callbackDelay) {
this.callbackDelay = callbackDelay;
}
}
package fi.codecrew.moya.beans.checkout;
public class CheckoutV2CustomerPojo {
private String email;
private String firstName;
private String lastName;
private String phone;
private String vatId;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getVatId() {
return vatId;
}
public void setVatId(String vatId) {
this.vatId = vatId;
}
}
package fi.codecrew.moya.beans.checkout;
public class CheckoutV2ItemPojo {
private int unitPrice ;
private int units;
private int vatPercentage;
private String productCode;
private String deliveryDate;
private String description;
private String category;
private String orderId;
private String stamp;
private String reference;
private String merchant;
// COmmission commission;
public int getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(int unitPrice) {
this.unitPrice = unitPrice;
}
public int getUnits() {
return units;
}
public void setUnits(int units) {
this.units = units;
}
public int getVatPercentage() {
return vatPercentage;
}
public void setVatPercentage(int vatPercentage) {
this.vatPercentage = vatPercentage;
}
public String getProductCode() {
return productCode;
}
public void setProductCode(String productCode) {
this.productCode = productCode;
}
public String getDeliveryDate() {
return deliveryDate;
}
public void setDeliveryDate(String deliveryDate) {
this.deliveryDate = deliveryDate;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getStamp() {
return stamp;
}
public void setStamp(String stamp) {
this.stamp = stamp;
}
public String getReference() {
return reference;
}
public void setReference(String reference) {
this.reference = reference;
}
public String getMerchant() {
return merchant;
}
public void setMerchant(String merchant) {
this.merchant = merchant;
}
}
package fi.codecrew.moya.beans.moyamessage;
import fi.codecrew.moya.beans.BotBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ejb.EJB;
import javax.ejb.MessageDriven;
import javax.jms.Message;
import javax.jms.MessageListener;
/**
* Message-Driven Bean implementation class for: IrcBotTopicListener
*/
@MessageDriven(mappedName = MoyaEventSender.MOYA_EVENT_SENDER_TOPIC)
public class SlackBotTopicListener implements MessageListener {
@SuppressWarnings("unused")
private static final Logger logger = LoggerFactory.getLogger(SlackBotTopicListener.class);
@EJB
private BotBean botbean;
/**
* @see MessageListener#onMessage(Message)
*/
public void onMessage(Message message) {
// botbean.getBot().say(toString(event, " ", msg.getEventtype(), " User
// ", msg.getDescription()));
if (botbean!= null) {
// botbean.message(message);
}
}
}
package fi.codecrew.moya.beans;
import com.google.gson.Gson;
import fi.codecrew.moya.beans.checkout.CheckoutCreateResponsePojo;
import fi.codecrew.moya.beans.checkout.CheckoutCredentials;
import fi.codecrew.moya.beans.checkout.CheckoutQueryBuilder;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.sql.Date;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
public class CheckoutFIV2BeanTest {
private static final CheckoutCredentials TEST_CREDS = new CheckoutCredentials(Date.from(Instant.now().plus(2, ChronoUnit.DAYS)), "375917", "SAIPPUAKAUPPIAS");
private static final Logger logger = LoggerFactory.getLogger(CheckoutFIV2BeanTest.class);
private static class TestCheckoutBuilder extends CheckoutQueryBuilder {
public TestCheckoutBuilder(CheckoutCredentials creds) {
super(creds);
}
@Override
public String getUri() {
return "/payments";
}
@Override
protected CheckoutQueryBuilder.Method getMethod() {
return CheckoutQueryBuilder.Method.POST;
}
}
// 'stamp' must be changed in json-file. The test db will be flushed daily
//@Test
public void createQuery() throws IOException {
CheckoutQueryBuilder qb = new TestCheckoutBuilder(TEST_CREDS);
String text = null;
try (InputStream testfile1 = getClass().getResourceAsStream("checkout-post-example1.json")) {
text = IOUtils.toString(testfile1, StandardCharsets.UTF_8.toString());
}
qb.setContent(text);
logger.info("Writing content {}", text);
CloseableHttpResponse response = qb.sendQuery();
logger.info("Received statuscode {}", response.getStatusLine());
try (InputStream queryContent = response.getEntity().getContent()) {
CheckoutCreateResponsePojo responsePojo = new Gson().fromJson(new InputStreamReader(queryContent), CheckoutCreateResponsePojo.class);
assertEquals(responsePojo.getHref(), "");
} catch (IOException e) {
assertTrue(false);
logger.warn("Error sending checkout.fi request", e);
}
}
private static final String TEST_VERIFY_STRING = "checkout-account=375917&checkout-algorithm=sha256&checkout-amount=2964&checkout-stamp=15336332710015&checkout-reference=192387192837195&checkout-transaction-id=4b300af6-9a22-11e8-9184-abb6de7fd2d0&checkout-status=ok&checkout-provider=nordea&signature=b2d3ecdda2c04563a4638fcade3d4e77dfdc58829b429ad2c2cb422d0fc64080";
@Test
public void testVerify() {
Map<String, String> fields = new HashMap<String, String>();
for (String s : TEST_VERIFY_STRING.split("&")) {
String[] parts = s.split("=");
assertEquals(parts.length, 2, "Part must be split to 2 parts" + s);
fields.put(parts[0], parts[1]);
}
String checksum = CheckoutQueryBuilder.generatesignature(TEST_CREDS.getMerchantPassword(), fields, "");
assertEquals(checksum.toLowerCase(), "b2d3ecdda2c04563a4638fcade3d4e77dfdc58829b429ad2c2cb422d0fc64080".toLowerCase());
}
@Test
public void testHmac() {
String testContent = "checkout-account:375917\n" +
"checkout-algorithm:sha256\n" +
"checkout-method:POST\n" +
"checkout-nonce:564635208570151\n" +
"checkout-timestamp:2018-07-06T10:01:31.904Z\n" +
"{\"stamp\":\"unique-identifier-for-merchant\",\"reference\":\"3759170\",\"amount\":1525,\"currency\":\"EUR\",\"language\":\"FI\",\"items\":[{\"unitPrice\":1525,\"units\":1,\"vatPercentage\":24,\"productCode\":\"#1234\",\"deliveryDate\":\"2018-09-01\"}],\"customer\":{\"email\":\"test.customer@example.com\"},\"redirectUrls\":{\"success\":\"https://ecom.example.com/cart/success\",\"cancel\":\"https://ecom.example.com/cart/cancel\"}}";
String hmac = CheckoutQueryBuilder.calculateHmac(TEST_CREDS.getMerchantPassword(), testContent);
//logger.info("Calculated hmac: {}", hmac);
assertEquals(hmac, "3708f6497ae7cc55a2e6009fc90aa10c3ad0ef125260ee91b19168750f6d74f6");
}
}
...@@ -5,7 +5,10 @@ import static org.testng.AssertJUnit.assertTrue; ...@@ -5,7 +5,10 @@ import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail; import static org.testng.AssertJUnit.fail;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
...@@ -16,7 +19,9 @@ import java.util.List; ...@@ -16,7 +19,9 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import com.google.common.io.CharStreams;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.testng.annotations.Test; import org.testng.annotations.Test;
...@@ -32,7 +37,7 @@ import fi.codecrew.moya.util.CheckoutBank; ...@@ -32,7 +37,7 @@ import fi.codecrew.moya.util.CheckoutBank;
public class CheckoutFiBeanTest { public class CheckoutFiBeanTest {
@Test @Test
public void testQuery() { public void testQuery() throws IOException {
new CheckoutFiBeanMock().testQuery(); new CheckoutFiBeanMock().testQuery();
} }
...@@ -43,8 +48,7 @@ public class CheckoutFiBeanTest { ...@@ -43,8 +48,7 @@ public class CheckoutFiBeanTest {
} }
@Test @Test
public void testPollXml() throws UnsupportedEncodingException public void testPollXml() throws UnsupportedEncodingException {
{
new CheckoutFiBeanMock().testPollXml(); new CheckoutFiBeanMock().testPollXml();
} }
......
{
"stamp": "12312313124",
"reference": "9187445",
"amount": 1590,
"currency": "EUR",
"language": "FI",
"items": [
{
"unitPrice": 1590,
"units": 1,
"vatPercentage": 24,
"productCode": "#927502759",
"deliveryDate": "2018-03-07",
"description": "Cat ladder",
"category": "Pet supplies",
"merchant": "375917",
"stamp": "29858472952",
"reference": "9187445"
}
],
"customer": {
"email": "erja.esimerkki@example.org",
"firstName": "Erja",
"lastName": "Esimerkki",
"phone": "+358501234567",
"vatId": "FI12345671"
},
"deliveryAddress": {
"streetAddress": "Eteläpuisto 2 C",
"postalCode": "33200",
"city": "Tampere",
"county": "Pirkanmaa",
"country": "FI"
},
"invoicingAddress": {
"streetAddress": "Gebhardinaukio 1",
"postalCode": "00510",
"city": "Helsinki",
"county": "Uusimaa",
"country": "FI"
},
"redirectUrls": {
"success": "https://ecom.example.org/success",
"cancel": "https://ecom.example.org/cancel"
},
"callbackUrls": {
"success": "https://ecom.example.org/success",
"cancel": "https://ecom.example.org/cancel"
}
}
\ No newline at end of file
...@@ -23,16 +23,17 @@ ...@@ -23,16 +23,17 @@
package fi.codecrew.moya.model; package fi.codecrew.moya.model;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne; import javax.persistence.OneToOne;
import javax.persistence.Table; import javax.persistence.Table;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
/** /**
* *
...@@ -89,6 +90,11 @@ public class BillLine extends GenericEntity { ...@@ -89,6 +90,11 @@ public class BillLine extends GenericEntity {
@JoinColumn(name = "foodwave_id", referencedColumnName = "id", nullable = true, updatable = false) @JoinColumn(name = "foodwave_id", referencedColumnName = "id", nullable = true, updatable = false)
private FoodWave foodwave; private FoodWave foodwave;
@ManyToOne
@JoinColumn(name = "discount_source_id", nullable = true, updatable = false)
private BillLine discountSource;
/** /**
* Calculate the total price for the items on this line, * Calculate the total price for the items on this line,
* includes vat * includes vat
...@@ -241,4 +247,11 @@ public class BillLine extends GenericEntity { ...@@ -241,4 +247,11 @@ public class BillLine extends GenericEntity {
this.foodwave = foodwave; this.foodwave = foodwave;
} }
public BillLine getDiscountSource() {
return discountSource;
}
public void setDiscountSource(BillLine discountSource) {
this.discountSource = discountSource;
}
} }
...@@ -62,7 +62,7 @@ public enum LanEventPropertyKey { ...@@ -62,7 +62,7 @@ public enum LanEventPropertyKey {
*/ */
EVENT_CURRENCY_CODE(Type.TEXT, "EUR"), EVENT_CURRENCY_CODE(Type.TEXT, "EUR"),
ALLOW_FREE_BILLS(Type.BOOL), ALLOW_FREE_BILLS(Type.BOOL),
; KEYCHECKOUT_V2_ENABLED(Type.BOOL, "1");
public enum Type { public enum Type {
TEXT, DATE, DATA, BOOL, LONG TEXT, DATE, DATA, BOOL, LONG
......
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html" xmlns:users="http://java.sun.com/jsf/composite/cditools/user"
xmlns:f="http://java.sun.com/jsf/core">
<h:body>
<ui:composition template="#{sessionHandler.template}">
<f:metadata>
<f:event type="preRenderView" listener="#{checkoutV2View.validate}"/>
</f:metadata>
<ui:define name="title">
<h1>#{i18n['page.checkout.cancel.header']}</h1>
</ui:define>
<ui:define name="content">
<p>
<h:outputText rendered="#{checkoutV2View.validationResult}" value="#{i18n['checkout.cancel.successMessage']}"/>
<h:outputText rendered="#{!checkoutV2View.validationResult}" value="#{i18n['checkout.cancel.errorMessage']}"/>
</p>
</ui:define>
</ui:composition>
</h:body>
</html>
\ No newline at end of file
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html" xmlns:users="http://java.sun.com/jsf/composite/cditools/user"
xmlns:f="http://java.sun.com/jsf/core">
<h:body>
<ui:composition template="#{sessionHandler.template}">
<f:metadata>
<f:event type="preRenderView" listener="#{checkoutV2View.validate}"/>
</f:metadata>
<ui:define name="title">
<h1>#{i18n['page.checkout.return.header']}</h1>
</ui:define>
<ui:define name="content">
<p>
<h:outputText rendered="#{checkoutV2View.validationResult}" value="#{i18n['checkout.return.successMessage']}"/>
<h:outputText rendered="#{!checkoutV2View.validationResult}" value="#{i18n['checkout.return.errorMessage']}"/>
</p>
</ui:define>
</ui:composition>
</h:body>
</html>
\ No newline at end of file
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
</script> </script>
</ui:fragment> </ui:fragment>
<ui:fragment rendered="#{billEditView.checkoutFiAvailable}"> <ui:fragment rendered="#{billEditView.checkoutFiAvailable}">
<table border="0"> <table border="0">
<tr> <tr>
...@@ -51,6 +50,25 @@ ...@@ -51,6 +50,25 @@
</tr> </tr>
</table> </table>
</ui:fragment> </ui:fragment>
<ui:fragment rendered="#{billEditView.checkoutFiV2Available}">
<table border="0">
<tr>
<ui:repeat varStatus="idx" value="#{billEditView.checkoutFiV2Token.providers}" var="provider">
<td>
<form action="#{provider.url}" method="post">
<ui:repeat value="#{provider.parameters}" var="provParam">
<input type="hidden" name="#{provParam.name}" value="#{provParam.value}" />
</ui:repeat>
<input type='image' src='#{provider.icon}' />
</form>
</td>
<h:outputText escape="false" value="&lt;/tr>&lt;tr>"
rendered="#{idx.index % 4 == 3}" />
</ui:repeat>
</tr>
</table>
</ui:fragment>
</composite:implementation> </composite:implementation>
</html> </html>
...@@ -18,7 +18,6 @@ ...@@ -18,7 +18,6 @@
*/ */
package fi.codecrew.moya.web.cdiview.shop; package fi.codecrew.moya.web.cdiview.shop;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.List; import java.util.List;
...@@ -26,6 +25,11 @@ import javax.ejb.EJB; ...@@ -26,6 +25,11 @@ import javax.ejb.EJB;
import javax.enterprise.context.ConversationScoped; import javax.enterprise.context.ConversationScoped;
import javax.inject.Named; import javax.inject.Named;
import fi.codecrew.moya.beans.CheckoutFiV2BeanLocal;
import fi.codecrew.moya.beans.EventBeanLocal;
import fi.codecrew.moya.beans.checkout.CheckoutCreateResponsePojo;
import fi.codecrew.moya.model.LanEventProperty;
import fi.codecrew.moya.model.LanEventPropertyKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -56,9 +60,14 @@ public class BillEditView extends GenericCDIView { ...@@ -56,9 +60,14 @@ public class BillEditView extends GenericCDIView {
@EJB @EJB
private transient CheckoutFiBeanLocal checkoutbean; private transient CheckoutFiBeanLocal checkoutbean;
@EJB @EJB
private transient CheckoutFiV2BeanLocal checkoutV2Bean;
@EJB
private transient BillBeanLocal billbean; private transient BillBeanLocal billbean;
@EJB
private transient EventBeanLocal eventbean;
private List<CheckoutBank> checkoutFiToken; private List<CheckoutBank> checkoutFiToken;
private CheckoutCreateResponsePojo checkoutFiV2Token;
private static final Logger logger = LoggerFactory.getLogger(BillEditView.class); private static final Logger logger = LoggerFactory.getLogger(BillEditView.class);
...@@ -77,9 +86,15 @@ public class BillEditView extends GenericCDIView { ...@@ -77,9 +86,15 @@ public class BillEditView extends GenericCDIView {
} }
} }
public boolean getCheckoutFiV2Available() {
boolean v2Enabled = eventbean.getPropertyBoolean(LanEventPropertyKey.KEYCHECKOUT_V2_ENABLED);
return v2Enabled && bill != null && !bill.isPaid() && checkoutV2Bean.isPaymentEnabled();
}
public boolean isCheckoutFiAvailable() public boolean isCheckoutFiAvailable()
{ {
return bill != null && !bill.isPaid() && checkoutbean.isPaymentEnabled(); boolean v2Enabled = eventbean.getPropertyBoolean(LanEventPropertyKey.KEYCHECKOUT_V2_ENABLED);
return !v2Enabled && bill != null && !bill.isPaid() && checkoutbean.isPaymentEnabled();
} }
public boolean isVerkkomaksuFiAvailable() public boolean isVerkkomaksuFiAvailable()
...@@ -98,6 +113,17 @@ public class BillEditView extends GenericCDIView { ...@@ -98,6 +113,17 @@ public class BillEditView extends GenericCDIView {
return checkoutFiToken; return checkoutFiToken;
} }
public CheckoutCreateResponsePojo getCheckoutFiV2Token()
{
if (bill != null && checkoutFiV2Token == null)
{
// checkoutFiToken = checkoutbean.testXml();
checkoutFiV2Token = checkoutV2Bean.createCheckoutBill(bill);
}
return checkoutFiV2Token;
}
public VerkkomaksutReturnEntry getVerkkomaksuFiToken() public VerkkomaksutReturnEntry getVerkkomaksuFiToken()
{ {
if (isVerkkomaksuFiAvailable() && bill != null && vmreturn == null) if (isVerkkomaksuFiAvailable() && bill != null && vmreturn == null)
......
/*
* Copyright Codecrew Ry
*
* All rights reserved.
*
* This license applies to any software containing a notice placed by the
* copyright holder. Such software is herein referred to as the Software.
* This license covers modification, distribution and use of the Software.
*
* Any distribution and use in source and binary forms, with or without
* modification is not permitted without explicit written permission from the
* copyright owner.
*
* A non-exclusive royalty-free right is granted to the copyright owner of the
* Software to use, modify and distribute all modifications to the Software in
* future versions of the Software.
*
*/
package fi.codecrew.moya.web.cdiview.shop;
import fi.codecrew.moya.beans.CheckoutFiBeanLocal;
import fi.codecrew.moya.beans.CheckoutFiV2BeanLocal;
import fi.codecrew.moya.util.CheckoutReturnType;
import fi.codecrew.moya.web.cdiview.GenericCDIView;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.faces.context.FacesContext;
import javax.faces.event.ComponentSystemEvent;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@Named
@RequestScoped
public class CheckoutV2View extends GenericCDIView {
@EJB
private transient CheckoutFiV2BeanLocal checkoutbean;
private boolean validationResult = false;
public void validate() {
HttpServletRequest origRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
Map<String, String> params = new HashMap<>();
Enumeration<String> paramIter = origRequest.getParameterNames();
while (paramIter.hasMoreElements()) {
String headername = paramIter.nextElement();
params.put(headername, origRequest.getParameter(headername));
}
validationResult = checkoutbean.verifyResponse(params);
}
public boolean getValidationResult() {
return validationResult;
}
}
...@@ -338,6 +338,9 @@ public class ProductShopView extends GenericCDIView { ...@@ -338,6 +338,9 @@ public class ProductShopView extends GenericCDIView {
} }
public String commitBillCart() { public String commitBillCart() {
if(billEditView.getBill() != null){
return "showCreatedBill";
}
for(ProductShopItem i : shoppingcart){ for(ProductShopItem i : shoppingcart){
psiHelper.updateProductShopItemCount(i); psiHelper.updateProductShopItemCount(i);
} }
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!