In this tutorial, we will be integrating PayuMoney payment gateway with a spring boot and Angular application. Here, we will be using Angular 6 but the same is also applicable for Angular 5 and 4. For server-side implementation, we will be using Spring boot 2 with Spring data and Mysql DB to save the transaction details. In my last article, we discussed integrating the same payment gateway with Spring Boot MVC and Angular 1. The server-side implementation is exactly the same as my last article and hence, we may skip some common configurations.
To get started with Payu, first we need to register on the Payu portal to generate a payment key and salt. This payment key and salt will be used to generate the hash using SHA-512 hash function while making the payment request to Payu. The payment flow which we will be developing here has 3 parts.
First, take the user input for product Info, name, email, phone and amount to be paid. Other input fields such as success URL, failure URL, Payment Key, Hash, transaction Id will be hidden in the HTML form.
Next, create an entry of this user input in our DB. Here, we are using Mysql DB. With these user inputs, create a unique transaction Id and generate a hash in the server side.
Next, submit the form to //test.payu.in/_payment where a user will be asked to enter all the card details and on the successful payment, the user will be redirected to a success URL that we had provided during form post to Payu. This will be a POST call from PAYU with a MihpayId. This will be generated by Payu.
Update the transaction as success in our system and redirect user to payment success page.
On payment failure, we can redirect user to transaction failed page.
For a quick integration guide, you can visit this PayU integration Dev guide.
Angular 6 Project Structure
Let us build our client first as we will be re-using the server implementation that we had done in our last article and hence we will be discussing the server implementation later in this article.
Execute following commands to generate angular 6 project in any location of your choice.
ng new payu-angular-client
Doing so, our angular 6 application is generated with TypeScript 2.7 & RxJS 6.RxJS 6. Now let us generate our payment component with below command.
ng g component payment
Following is our Angular app strucuture.
Angular Payment Component
Let us first define our payment.component.html which has a simple form to take payment related inputs from the user. On submit, the form is submitted to a Payu test server in a new window.
payment.component.html<div class="col-md-4"> <h2>PayU Payment Form</h2> <form ngNoForm action="https://test.payu.in/_payment" name="payuform" method="POST" target="payment_popup" onsubmit="window.open('about:blank','payment_popup','width=900,height=500');"> <div class="form-group"> <label for="productInfo">Product Name:</label> <input type="text" class="form-control" id="productInfo" name="productinfo" [(ngModel)]="payuform.productinfo"> </div> <div class="form-group"> <label for="firstname">Name:</label> <input type="text" class="form-control" id="firstname" name="firstname" [(ngModel)]="payuform.firstname"> </div> <div class="form-group"> <label for="email">Email:</label> <input type="email" class="form-control" id="email" name="email" [(ngModel)]="payuform.email"> </div> <div class="form-group"> <label for="email">Phone:</label> <input type="number" class="form-control" id="phone" name="phone" [(ngModel)]="payuform.phone"> </div> <div class="form-group"> <label for="amount">Amount:</label> <input type="number" class="form-control" id="amount" name="amount" [(ngModel)]="payuform.amount"> </div> <textarea name="surl" id="surl" ng-model="surl" rows="2" cols="2" hidden [(ngModel)]="payuform.surl"></textarea> <textarea name="furl" id="furl" ng-model="furl" rows="2" cols="2" hidden [(ngModel)]="payuform.furl"></textarea> <textarea name="key" id="key" ng-model="key" rows="2" cols="2" hidden [(ngModel)]="payuform.key"></textarea> <textarea name="hash" id="hash" ng-model="hash" rows="2" cols="2" hidden [(ngModel)]="payuform.hash"></textarea> <textarea name="txnid" id="txnid" ng-model="txnid" rows="2" cols="2" hidden [(ngModel)]="payuform.txnid"></textarea> <textarea name="service_provider" id="service_provider" rows="2" cols="2" hidden></textarea> <button *ngIf="disablePaymentButton" type="button" class="btn btn-default" (click)="confirmPayment()">Confirm</button> <button *ngIf="!disablePaymentButton" type="submit" class="btn btn-danger">Submit</button> </form> </div>
Now, let us define our .ts file that has the implementation to make the API call to fetch
payment.component.tsimport { Component, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-payment', templateUrl: './payment.component.html', styleUrls: ['./payment.component.css'] }) export class PaymentComponent implements OnInit { public payuform: any = {}; disablePaymentButton: boolean = true; constructor(private http: HttpClient) { } confirmPayment() { const paymentPayload = { email: this.payuform.email, name: this.payuform.firstname, phone: this.payuform.phone, productInfo: this.payuform.productinfo, amount: this.payuform.amount } return this.http.post<any>('http://localhost:8080/payment/payment-details', paymentPayload).subscribe( data => { console.log(data); this.payuform.txnid = data.txnId; this.payuform.surl = data.sUrl; this.payuform.furl = data.fUrl; this.payuform.key = data.key; this.payuform.hash = data.hash; this.payuform.txnid = data.txnId; this.disablePaymentButton = false; }, error1 => { console.log(error1); }) } ngOnInit() { } }
Following is the implementation of our app.module.ts.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { PaymentComponent } from './payment/payment.component'; import {RouterModule} from "@angular/router"; import {FormsModule} from "@angular/forms"; import {HttpClientModule} from "@angular/common/http"; @NgModule({ declarations: [ AppComponent, PaymentComponent ], imports: [ BrowserModule, FormsModule, HttpClientModule, RouterModule.forRoot([ {path: '', component: PaymentComponent} ]) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Server Side Implementation
Following is the project structure of the spring boot app that we will be building.
Maven Dependency
pom.xml<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Server Side Implementation
Following is our controller class. Method proceedPayment() will save the payment details in the DB.
The method payuCallback() will be a callback method which will be executed as a callback method on the either payment success or failure.
PaymentController.java@Controller @RequestMapping("/payment") public class PaymentController { @Autowired private PaymentService paymentService; @PostMapping(path = "/payment-details") public @ResponseBody PaymentDetail proceedPayment(@RequestBody PaymentDetail paymentDetail){ return paymentService.proceedPayment(paymentDetail); } @RequestMapping(path = "/payment-response", method = RequestMethod.POST) public @ResponseBody String payuCallback(@RequestParam String mihpayid, @RequestParam String status, @RequestParam PaymentMode mode, @RequestParam String txnid, @RequestParam String hash){ PaymentCallback paymentCallback = new PaymentCallback(); paymentCallback.setMihpayid(mihpayid); paymentCallback.setTxnid(txnid); paymentCallback.setMode(mode); paymentCallback.setHash(hash); paymentCallback.setStatus(status); return paymentService.payuCallback(paymentCallback); } }
Following is the model class to map user input in server side.
PaymentDetail.javapublic class PaymentDetail { private String email; private String name; private String phone; private String productInfo; private String amount; private String txnId; private String hash; private String sUrl; private String fUrl; private String key; //setters and getters
Similarly, following is the PaymentCallback model class.
PaymentCallback.javapublic class PaymentCallback { private String txnid; private String mihpayid; private PaymentMode mode; private String status; private String hash; //setters and getters
Now, let us define our service class. Here, proceedPayment() will internally call savePaymentDetail() to save the payment details in the DB and it calls the util method to populate payment related information such as key, hash and other details. Similarly, payuCallback() will check the status and based on that it will update the transaction status in DB.
PaymentServiceImpl.java@Service public class PaymentServiceImpl implements PaymentService { @Autowired private PaymentRepo paymentRepository; @Override public PaymentDetail proceedPayment(PaymentDetail paymentDetail) { PaymentUtil paymentUtil = new PaymentUtil(); paymentDetail = paymentUtil.populatePaymentDetail(paymentDetail); savePaymentDetail(paymentDetail); return paymentDetail; } @Override public String payuCallback(PaymentCallback paymentResponse) { String msg = "Transaction failed."; Payment payment = paymentRepository.findByTxnId(paymentResponse.getTxnid()); if(payment != null) { //TODO validate the hash PaymentStatus paymentStatus = null; if(paymentResponse.getStatus().equals("failure")){ paymentStatus = PaymentStatus.Failed; }else if(paymentResponse.getStatus().equals("success")) { paymentStatus = PaymentStatus.Success; msg = "Transaction success"; } payment.setPaymentStatus(paymentStatus); payment.setMihpayId(paymentResponse.getMihpayid()); payment.setMode(paymentResponse.getMode()); paymentRepository.save(payment); } return msg; } private void savePaymentDetail(PaymentDetail paymentDetail) { Payment payment = new Payment(); payment.setAmount(Double.parseDouble(paymentDetail.getAmount())); payment.setEmail(paymentDetail.getEmail()); payment.setName(paymentDetail.getName()); payment.setPaymentDate(new Date()); payment.setPaymentStatus(PaymentStatus.Pending); payment.setPhone(paymentDetail.getPhone()); payment.setProductInfo(paymentDetail.getProductInfo()); payment.setTxnId(paymentDetail.getTxnId()); paymentRepository.save(payment); } }
Following is our entity class.
Payment.java@Entity @Table(name = "Payment") public class Payment { @Id @Column @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column private String email; @Column private String name; @Column private String phone; @Column private String productInfo; @Column private Double amount; @Column @Enumerated(EnumType.STRING) private PaymentStatus paymentStatus; @Column @Temporal(TemporalType.DATE) private Date paymentDate; @Column private String txnId; @Column private String mihpayId; @Column @Enumerated(EnumType.STRING) private PaymentMode mode;PaymentMode.java
public enum PaymentMode {
NB,DC,CC
}
PaymentStatus.java
public enum PaymentStatus {
Pending,Failed,Success
}
Following is the PaymentUtil.java that has logic to generate the hash and an unique transaction id. While generating hash we need to understand the hash sequence. The sequence is predefined by PayU and it is available on the Dev guide. We are following the same sequence here. PayU also provides provision to add user defined information but since we don't have any user defined terms here the sequence is empty. We are only using compulsory parameters here to make our example simple. Make sure you alo append the salt at the end od the sequence.
As per PayU,
hashSequence = key|txnid|amount|productinfo|firstname|email|udf1|udf2|udf3|udf4|udf5||||||salt. $hash = hash("sha512", $hashSequence); Where salt is available on the PayUMoney dashboard.PaymentUtil.java
public class PaymentUtil { private static final String paymentKey = "your-key"; private static final String paymentSalt = "your-salt"; private static final String sUrl = "http://localhost:8080/payment/payment-response"; private static final String fUrl = "http://localhost:8080/payment/payment-response"; public static PaymentDetail populatePaymentDetail(PaymentDetail paymentDetail){ String hashString = ""; Random rand = new Random(); String randomId = Integer.toString(rand.nextInt()) + (System.currentTimeMillis() / 1000L); String txnId = "Dev" + hashCal("SHA-256", randomId).substring(0, 12); paymentDetail.setTxnId(txnId); String hash = ""; //String otherPostParamSeq = "phone|surl|furl|lastname|curl|address1|address2|city|state|country|zipcode|pg"; String hashSequence = "key|txnid|amount|productinfo|firstname|email|||||||||||"; hashString = hashSequence.concat(paymentSalt); hashString = hashString.replace("key", paymentKey); hashString = hashString.replace("txnid", txnId); hashString = hashString.replace("amount", paymentDetail.getAmount()); hashString = hashString.replace("productinfo", paymentDetail.getProductInfo()); hashString = hashString.replace("firstname", paymentDetail.getName()); hashString = hashString.replace("email", paymentDetail.getEmail()); hash = hashCal("SHA-512", hashString); paymentDetail.setHash(hash); paymentDetail.setfUrl(fUrl); paymentDetail.setsUrl(sUrl); paymentDetail.setKey(paymentKey); return paymentDetail; } public static String hashCal(String type, String str) { byte[] hashseq = str.getBytes(); StringBuffer hexString = new StringBuffer(); try { MessageDigest algorithm = MessageDigest.getInstance(type); algorithm.reset(); algorithm.update(hashseq); byte messageDigest[] = algorithm.digest(); for (int i = 0; i < messageDigest.length; i++) { String hex = Integer.toHexString(0xFF & messageDigest[i]); if (hex.length() == 1) { hexString.append("0"); } hexString.append(hex); } } catch (NoSuchAlgorithmException nsae) { } return hexString.toString(); } }
Following is the application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/testdb spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update
We have very simple implementation of repository. We have only one method that pulls transaction details based on transaction id.
PaymentRepo.java@Repository public interface PaymentRepo extends CrudRepository{ Payment findByTxnId(String txnId); }
SQL Scripts
create table payment (id integer not null auto_increment, amount double precision, email varchar(255), mihpay_id varchar(255), mode varchar(255), name varchar(255), payment_date date, payment_status varchar(255), phone varchar(255), product_info varchar(255), txn_id varchar(255), primary key (id)) engine=MyISAM;
Following is our PayumoneyIntegrationServerApplication.java that will bootstrap our spring boot app.
@SpringBootApplication public class PayumoneyIntegrationServerApplication extends SpringBootServletInitializer { private static Class applicationClass = PayumoneyIntegrationServerApplication.class; public static void main(String[] args) { SpringApplication.run(PayumoneyIntegrationServerApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(applicationClass); } }
Testing the payment Integration
Let us run PayumoneyIntegrationServerApplication.java as a java application and Angular app with ng serve and hit localhost:4200
After entering the details, hit submit button and you will be redirected to PayU payment page in a new window.
Once, the payment is successful, you will be redirected to http://localhost:8080/payment/payment-response and based on the status the DB will be updated.
Following is the complete response from PayU.
mihpayid=109734&mode=CC&status=failure&unmappedstatus=failed&key=gtKFFx&txnid=QDSb57a12610be90a46&amount=5000.00&cardCategory=domestic&discount=0.00&net_amount_debit=0.00&addedon=2018-07-19+20%3A47%3A24&productinfo=dataSupport&firstname=Dhiraj&lastname=&address1=&address2=&city=&state=&country=&zipcode=&email=only2dhir%40gmail.com&phone=8884377251&udf1=&udf2=&udf3=&udf4=&udf5=&udf6=&udf7=&udf8=&udf9=&udf10=&hash=9044307d31d092e99a729710b2fedd387f31ee0d3f1dcb55dd3fc68eda1c1b6ae1d7382eb9a4c565a0c08fd6caea16544c2bc7106a68fcb602f15e3c878f67c7&field1=152536&field2=971578&field3=20180719&field4=MC&field5=794708900428&field6=45&field7=1&field8=3DS&field9=+Verification+of+Secure+Hash+Failed%3A+E700+--+Unspecified+Failure+--+Unknown+Error+--+Unable+to+be+determined--E500&payment_source=payu&PG_TYPE=AXISPG&bank_ref_num=152536&bankcode=CC&error=E500&error_Message=Bank+failed+to+authenticate+the+customer&name_on_card=Test&cardnum=401200XXXXXX1112&cardhash=This+field+is+no+longer+supported+in+postback+params.&issuing_bank=AXIS&card_type=VISAnull querystring
Also, you will receive a notification email about the status of the payment in the mailbox that have given on payment page.
Conclusion
In this article, we created a sample spring boot and angular 6 example and integrated PayU payment gateway with it.