diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 551de5b13..c6bc62b4c 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -111,6 +111,7 @@
  • Full API Matrix
  • Beta API Matrix
  • Payment Application Logic / Transaction Processing Strategies
  • +
  • Self Service API
  • @@ -3005,6 +3006,219 @@ + +
    +

    Self Service

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    RESOURCES
    +
    + + + + + + +
    POST
    create/complex + update
    + + + + + + +
    GET
    read
    + + + + + + +
    PUT
    update
    + + + + + + +
    DELETE
    delete
    Authentication HTTP Basicself/authenticationVerify Authentication
    Authentication Oauth2self/userdetailsFetch Authenticated User Details
    Clientsself/clientsList Clients
    self/clients/{clientId}Retrieve Client
    self/clients/{clientId}/accountsRetrieve Client Accounts Overview
    self/clients/{clientId}/imagesRetrieve Client Image
    self/clients/{clientId}/chargesList Client Charges
    self/clients/{clientId}/charges/{chargeId}Retrieve a Client Charge
    self/clients/{clientId}/transactionsList Client Transactions
    self/clients/{clientId}/transactions/{transactionId}Retrieve a Client Transaction
    Loansself/loans/{loanId}Retrieve a Loan
    self/loans/{loanId}/transactions/{transactionId}Retrieve a Loan Transaction Details
    self/loans/{loanId}/chargesRetrieve Loan Charges
    self/loans/{loanId}/charges/{chargeId}Retrieve a Loan Charge Details
    Savingsself/savingsaccounts/{accountId}Retrieve a Savings Account
    self/savingsaccounts/{accountId}/transactions/{transactionId}Retrieve a Savings Transaction Details
    self/savingsaccounts/{accountId}/chargesRetrieve Savings Charges
    self/savingsaccounts/{accountId}/charges/{savingsAccountChargeId}Retrieve a Savings Charge Details
    Own Account Transfersself/accounttransfers/templateAccount Transfer Template
    self/accounttransfersAccount Transfer
    +
    +
    +
    +
    @@ -3763,7 +3977,28 @@ Successful transaction response:
    +   +
    +
    +

    Self Service API Overview

    +

    + Self Service APIs are a set of APIs with restricted data scope. Functional specifications and design can be viewed + here. +

    +

    + While creating an user, user can be tagged as self service user. Also you can associate + clients that this user has access to. Data scope is restricted to these linked clients. +

    +

    + A self service user shall have access to only self service APIs. Self service APIs cannot be accessed by non-self service user. Vice-versa is also true. +

    +
    +
    + +
    +
     
    @@ -31382,7 +31617,7 @@ GET https://DomainName/api/v1/users/{userId}
    Optional Fields
    - staffId,passwordNeverExpires + staffId,passwordNeverExpires,isSelfServiceUser,clients
    @@ -31432,6 +31667,32 @@ Request body: { "officeId": 1, "resourceId": 12 +} + + +POST https://DomainName/api/v1/users + + +POST users +Content-Type: application/json +Request body: +{ + "username": "newuser", + "firstname": "Test", + "lastname": "User", + "email": "whatever@mifos.org", + "officeId": 1, + "staffId": 1, + "roles": [2,3], + "sendPasswordToEmail": true, + "isSelfServiceUser": true, + "clients": [1,2,3] +} + + +{ + "officeId": 1, + "resourceId": 11 } @@ -38077,6 +38338,1778 @@ No Request Body + + +
    +
    +

    Self Service APIs

    +
    +
    +   +
    +
    +

    Verify authentication

    +

    Authenticates the credentials provided and returns the set roles and permissions allowed.

    +
    +
    + +POST https://DomainName/api/v1/self/authentication?username={username}&password={password} + + +POST self/authentication?username=mifos&password=password +Content-Type: application/json +No Request Body + +

    Example response of autentication for user

    + +{ + "username": "mifos", + "userId": 1, + "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", + "authenticated": true, + "officeId": 1, + "officeName": "Head Office", + "staffId": 1, + "staffDisplayName": "Director, Program", + "organisationalRole": { + "id": 100, + "code": "staffOrganisationalRoleType.programDirector", + "value": "Program Director" + }, + "roles": [ + { + "id": 1, + "name": "Super user", + "description": "This role provides all application permissions." + } + ], + "permissions": [ + "ALL_FUNCTIONS" + ], + "isSelfServiceUser": true, + "clients": [1,2,3] +} + + + +POST self/authentication?username=mifos&password=fail +Content-Type: application/json +No Request Body + + +{ + "developerMessage": "Invalid authentication details were passed in api request.", + "developerDocLink": "https://github.com/openMF/mifosx/wiki/HTTP-API-Error-codes", + "httpStatusCode": "401", + "defaultUserMessage": "Unauthenticated. Please login.", + "userMessageGlobalisationCode": "error.msg.not.authenticated", + "errors": [] +} + +
    +
    + +   +
    +
    +

    Fetch authenticated user details

    +

    checks the Authentication and returns the set roles and permissions allowed.

    +
    +
    + +POST https://DomainName/api/v1/userdetails?access_token={access_token} + + +POST userdetails?access_token=bWlmb3M6cGFzc3dvcmQ= +Content-Type: application/json +No Request Body + +

    Example response of authenticated user.

    + +{ + "username": "mifos", + "userId": 1, + "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", + "authenticated": true, + "officeId": 1, + "officeName": "Head Office", + "staffId": 1, + "staffDisplayName": "Director, Program", + "organisationalRole": { + "id": 100, + "code": "staffOrganisationalRoleType.programDirector", + "value": "Program Director" + }, + "roles": [ + { + "id": 1, + "name": "Super user", + "description": "This role provides all application permissions." + } + ], + "permissions": [ + "ALL_FUNCTIONS" + ], + "isSelfServiceUser": true, + "clients": [1,2,3] +} + + + +POST api/oauth/token?username=mifos&password=password&client_id=community-app&grant_type=password&client_secret=123 +Content-Type: application/json +No Request Body + + +{ + "developerMessage": "Invalid authentication details were passed in api request.", + "developerDocLink": "https://github.com/openMF/mifosx/wiki/HTTP-API-Error-codes", + "httpStatusCode": "401", + "defaultUserMessage": "Unauthenticated. Please login.", + "userMessageGlobalisationCode": "error.msg.not.authenticated", + "errors": [] +} + +
    +
    + +   +
    +
    +

    List Clients associated to the user

    +

    The list capability of clients can support pagination and sorting.

    +
    Optional Arguments
    +
    +
    offset
    +
    + Integer optional, defaults to 0 +
    +
    Indicates the result from which pagination starts
    + +
    limit
    +
    + Integer optional, defaults to 200 +
    +
    Restricts the size of results returned. To override the default and return all entries you must explicitly pass a non-positive integer value for limit e.g. limit=0, or limit=-1
    + +
    orderBy
    +
    + String optional, one of displayName, accountNo, officeId, officeName +
    +
    Orders results by the indicated field.
    + +
    sortBy
    +
    + String optional, one of ASC, DESC +
    +
    Indicates what way to order results if orderBy is used.
    + +
    displayName
    +
    + String optional +
    +
    Use displayName of clients to restrict results.
    + +
    firstName
    +
    + String optional +
    +
    Use firstName of clients to restrict results.
    + +
    lastName
    +
    + String optional +
    +
    Use lastName of clients to restrict results.
    + +
    +

    Example Requests:

    +
    self/clients
    +
    +
    self/clients?fields=displayName,officeName
    +
    +
    self/clients?offset=10&limit=50
    +
    +
    self/clients?orderBy=displayName&sortOrder=DESC
    +
    +
    + +GET https://DomainName/api/v1/self/clients + + +{ + "totalFilteredRecords": 2, + "pageItems": [ + { + "id": 1, + "accountNo": "000000001", + "status": { + "id": 300, + "code": "clientStatusType.active", + "value": "Active" + }, + "active": true, + "activationDate": [ + 2013, + 3, + 1 + ], + "fullname": "Small shop", + "displayName": "Small shop", + "officeId": 1, + "officeName": "Head Office" + }, + { + "id": 2, + "accountNo": "000000002", + "status": { + "id": 100, + "code": "clientStatusType.pending", + "value": "Pending" + }, + "active": false, + "fullname": "Home Farm Produce", + "displayName": "Home Farm Produce", + "officeId": 1, + "officeName": "Head Office" + } + ] +} + +
    +
    + +   +
    +
    +

    Retrieve a Client

    +

    Example Requests:

    +
    self/clients/1
    +
    +
    self/clients/1?fields=id,displayName,officeName
    +
    +
    + +GET https://DomainName/api/v1/self/clients/{clientId} + + +{ + "id": 27, + "accountNo": "000000027", + "status": { + "id": 300, + "code": "clientStatusType.active", + "value": "Active" + }, + "active": true, + "activationDate": [ + 2013, + 1, + 1 + ], + "firstname": "savings", + "lastname": "test", + "displayName": "savings test", + "officeId": 1, + "officeName": "Head Office", + "timeline": { + "submittedOnDate": [ + 2013, + 1, + 1 + ], + "submittedByUsername": "mifos", + "submittedByFirstname": "App", + "submittedByLastname": "Administrator", + "activatedOnDate": [ + 2013, + 1, + 1 + ], + "activatedByUsername": "mifos", + "activatedByFirstname": "App", + "activatedByLastname": "Administrator" + }, + "savingsProductId": 4, + "savingsProductName": "account overdraft", + "groups": [] +} + +
    +
    +   +
    +
    +

    Retrieve client accounts overview

    +

    + An example of how a loan portfolio summary can be provided. This + is requested in a specific use case of the community application.
    It is quite reasonable to add resources like this to simplify User Interface development. +

    +

    Example Requests:

    +
    self/clients/1/accounts
    +
    +
    +
    self/clients/1/accounts?fields=loanAccounts,savingsAccounts
    +
    +
    + +GET https://DomainName/api/v1/self/clients/{clientId}/accounts + + +{ + "loanAccounts": [ + { + "id": 1, + "accountNo": "000000001", + "externalId": "456", + "productId": 1, + "productName": "TestOne", + "status": { + "id": 300, + "code": "loanStatusType.active", + "value": "Active", + "pendingApproval": false, + "waitingForDisbursal": false, + "active": true, + "closedObligationsMet": false, + "closedWrittenOff": false, + "closedRescheduled": false, + "closed": false, + "overpaid": false + }, + "loanType": { + "id": 1, + "code": "loanType.individual", + "value": "Individual" + }, + "loanCycle": 1 + } + ], + "savingsAccounts": [ + { + "id": 7, + "accountNo": "000000007", + "productId": 2, + "productName": "Other product", + "status": { + "id": 100, + "code": "savingsAccountStatusType.submitted.and.pending.approval", + "value": "Submitted and pending approval", + "submittedAndPendingApproval": true, + "approved": false, + "rejected": false, + "withdrawnByApplicant": false, + "active": false, + "closed": false + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + } + }, + { + "id": 6, + "accountNo": "000000006", + "productId": 1, + "productName": "Passbook Savings", + "status": { + "id": 300, + "code": "savingsAccountStatusType.active", + "value": "Active", + "submittedAndPendingApproval": false, + "approved": false, + "rejected": false, + "withdrawnByApplicant": false, + "active": true, + "closed": false + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "accountBalance": 1828.03 + }, + { + "id": 5, + "accountNo": "000000005", + "productId": 1, + "productName": "Passbook Savings", + "status": { + "id": 400, + "code": "savingsAccountStatusType.withdrawn.by.applicant", + "value": "Withdrawn by applicant", + "submittedAndPendingApproval": false, + "approved": false, + "rejected": false, + "withdrawnByApplicant": true, + "active": false, + "closed": true + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + } + } + ] +} + +
    +
    + +   +
    +
    +

    Retrieve Client Image

    +

    Optional arguments are identical to those of Get Image associated with an Entity (Binary file)

    +

    Example Requests:

    +
    self/clients/1/images
    +
    +
    +
    + +GET https://DomainName/api/v1/self/clients/{clientId}/images +Accept: text/plain + + + +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ +bWFnZVJlYWR5ccllPAAAAJ1JREFUeNpi+P//PwMIA4E9EG8E4idQDGLbw+WhiiqA+D8OXAFVAzbp +DxBvB2JLIGaGYkuoGEjOhhFIHAbij0BdPgxYACMj42ogJQpifwBiXSDeC8JIbt4LxSC5DyxQjTeB ++BeaYb+Q5EBOAVutCzMJHUNNPADzzDokiYdAfAmJvwLkGeTgWQfyKZICS6hYBTwc0QL8ORSjBDhA +gAEAOg13B6R/SAgAAAAASUVORK5CYII= + +
    +
    + +   +
    +
    +

    List Client Charges

    + +

    The list capability of client charges supports pagination.

    +
    Optional Arguments
    +
    +
    offset
    +
    + Integer Mandatory, defaults to 0 +
    +
    Indicates the result from which pagination starts
    + +
    limit
    +
    + Integer Mandatory, defaults to 200 +
    +
    Restricts the size of results returned. To override the default and return all entries you must explicitly pass a non-positive integer value for limit e.g. limit=0, or limit=-1
    + +
    pendingPayment
    +
    + String optional +
    +
    Filters charges that are pending payment (neither paid or waived).
    + +
    chargeStatus
    +
    + String optional +
    +
    Filters charges according to the status.
    +
    +

    Example Requests:

    +
    self/clients/1/charges
    +
    self/clients/1/charges?offset=0&limit=5
    +
    +
    + +GET https://DomainName/api/v1/self/clients/{clientId}/charges?limit=5&offset=0 + + +GET self/clients/1/charges?limit=5&offset=0 +Content-Type: application/json +No Request Body + + +{ + "totalFilteredRecords": 4, + "pageItems": [ + { + "id": 5, + "clientId": 1, + "chargeId": 6, + "name": "Client Fees 2", + "chargeTimeType": { + "id": 2, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "dueDate": [ + 2015, + 9, + 1 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 550.000000, + "amountPaid": 0.000000, + "amountWaived": 0.000000, + "amountWrittenOff": 0, + "amountOutstanding": 550.000000, + "penalty": false, + "isActive": true, + "isPaid": false, + "isWaived": false + }, + { + "id": 4, + "clientId": 1, + "chargeId": 5, + "name": "Client Fee 1", + "chargeTimeType": { + "id": 2, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "dueDate": [ + 2015, + 8, + 31 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 120.000000, + "amountPaid": 0.000000, + "amountWaived": 120.000000, + "amountWrittenOff": 0, + "amountOutstanding": 120.000000, + "penalty": true, + "isActive": true, + "isPaid": false, + "isWaived": true + }, + { + "id": 3, + "clientId": 1, + "chargeId": 5, + "name": "Client Fee 1", + "chargeTimeType": { + "id": 2, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "dueDate": [ + 2015, + 8, + 17 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100.000000, + "amountPaid": 0.000000, + "amountWaived": 100.000000, + "amountWrittenOff": 0, + "amountOutstanding": 0.000000, + "penalty": true, + "isActive": true, + "isPaid": false, + "isWaived": true + }, + { + "id": 2, + "clientId": 1, + "chargeId": 2, + "name": "Recurring savings Charge", + "chargeTimeType": { + "id": 7, + "code": "chargeTimeType.monthlyFee", + "value": "Monthly Fee" + }, + "dueDate": [ + 2015, + 8, + 17 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100.000000, + "amountPaid": 0, + "amountWaived": 100.000000, + "amountWrittenOff": 0, + "amountOutstanding": 0.000000, + "penalty": false, + "isActive": true, + "isPaid": false, + "isWaived": true + } + ] +} + + +
    +
    +   +
    +
    +

    Retrieve a Client Charge

    +
    Arguments
    +
    +
    associations
    +
    + optional, a comma separated list of + 'associations' (itemised below). +
    +
    +
    Associations are just extra pieces of data that you + might or might not want to retrieve.
    +
    +
    +
    + 'transactions': Retrieves all transactions made on this client charge. +
    +
    +

    Example Requests:

    +
    self/clients/1/charges/1
    +
    +
    +
    self/clients/1/charges/1?fields=name,id
    +
    +
    + +GET https://DomainName/api/v1/self/clients/{clientId}/charges/{clientChargeId} + + +{ + "id": 3, + "clientId": 1, + "chargeId": 5, + "name": "Client Fee 1", + "chargeTimeType": { + "id": 2, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "dueDate": [ + 2015, + 8, + 17 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100.000000, + "amountPaid": 0.000000, + "amountWaived": 100.000000, + "amountWrittenOff": 0, + "amountOutstanding": 0.000000, + "penalty": true, + "isActive": true, + "isPaid": false, + "isWaived": true +} + +
    +
    + +   +
    +
    +

    List Client Transactions

    +

    The list capability of client transaction can support pagination.

    +
    Mandatory Arguments
    +
    +
    offset
    +
    + Integer Mandatory, defaults to 0 +
    +
    Indicates the result from which pagination starts
    + +
    limit
    +
    + Integer Mandatory, defaults to 200 +
    +
    Restricts the size of results returned. To override the default and return all entries you must explicitly pass a non-positive integer value for limit e.g. limit=0, or limit=-1
    +
    +

    Example Requests:

    +
    self/clients/189/transactions
    +
    self/clients/189/transactions?offset=10&limit=50
    +
    +
    + +GET https://DomainName/api/v1/self/clients/{clientId}/transactions?limit=5&offset=0 + + +GET self/clients/226/transactions?limit=5&offset=0 +Content-Type: application/json +No Request Body: + + + +{ + "totalFilteredRecords": 20, + "pageItems": [ + { + "id": 226, + "officeId": 1, + "officeName": "Head Office", + "type": { + "id": 1, + "code": "clientTransactionType.payCharge", + "value": "PAY_CHARGE" + }, + "date": [ + 2015, + 9, + 2 + ], + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 22, + "submittedOnDate": [ + 2015, + 9, + 2 + ], + "reversed": false + } + ] +} + +
    +
    + +   +
    +
    +

    Retrieve a Client Transaction

    +

    Example Requests:

    +
    self/clients/1/transactions/1
    +
    +
    +
    self/clients/1/transactions/1?fields=id,officeName
    +
    +
    + +GET https://DomainName/api/v1/self/clients/{clientId}/transaction/{transactionId} + + +{ + "id": 1, + "officeId": 1, + "officeName": "Head Office", + "type": { + "id": 1, + "code": "clientTransactionType.payCharge", + "value": "PAY_CHARGE" + }, + "date": [ + 2015, + 8, + 17 + ], + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 60.000000, + "submittedOnDate": [ + 2015, + 8, + 19 + ], + "reversed": true +} + +
    +
    + +   +
    +
    +

    Retrieve a Loan

    +
    Arguments
    +
    +
    associations
    +
    + optional, a comma separated list of + loan 'associations' (itemised below). +
    +
    +
    Associations are just extra pieces of data that you + might or might not want to retrieve.
    +
    +
    +
    + 'repaymentSchedule': Loan schedule data. +
    +
    + 'originalSchedule': Loan schedule data without interest recalculations. +
    +
    + 'futureSchedule': Loan schedule data from today date(will be displayed only for interest first repayment strategy processors) +
    +
    + 'transactions': Loan transactions data. +
    +
    + 'charges': Loan charges data. +
    +
    + 'guarantors': Loan guarantors data. +
    +
    + 'collateral': Loan collateral data. +
    +
    + 'linkedAccount': Account linked to loan. +
    +
    + 'multiDisburseDetails': Loan multi-disbursement details. +
    +
    +

    Example Requests:

    +
    self/loans/1
    +
    +
    +
    self/loans/1?fields=id,principal,annualInterestRate
    +
    +
    +
    self/loans/1?fields=id,principal,annualInterestRate&associations=repaymentSchedule,transactions
    +
    +
    + +GET https://DomainName/api/v1/self/loans/{loanId} + + +{ + "id": 1, + "accountNo": "000000001", + "status": { + "id": 300, + "code": "loanStatusType.active", + "value": "Active", + "pendingApproval": false, + "waitingForDisbursal": false, + "active": true, + "closedObligationsMet": false, + "closedWrittenOff": false, + "closedRescheduled": false, + "closed": false, + "overpaid": false + }, + "clientId": 1, + "clientName": "Kampala first Client", + "clientOfficeId": 2, + "loanProductId": 1, + "loanProductName": "Kampala Product (with cash accounting)", + "loanProductDescription": "Typical Kampala loan product with cash accounting enabled for testing.", + "loanPurposeId": 22, + "loanPurposeName": "option.HousingImprovement", + "loanOfficerId": 2, + "loanOfficerName": "LoanOfficer, Kampala", + "loanType": { + "id": 1, + "code": "loanType.individual", + "value": "Individual" + }, + "currency": { + "code": "UGX", + "name": "Uganda Shilling", + "decimalPlaces": 2, + "displaySymbol": "USh", + "nameCode": "currency.UGX", + "displayLabel": "Uganda Shilling (USh)" + }, + "principal": 1000000, + "termFrequency": 12, + "termPeriodFrequencyType": { + "id": 2, + "code": "termFrequency.periodFrequencyType.months", + "value": "Months" + }, + "numberOfRepayments": 12, + "repaymentEvery": 1, + "repaymentFrequencyType": { + "id": 2, + "code": "repaymentFrequency.periodFrequencyType.months", + "value": "Months" + }, + "interestRatePerPeriod": 24, + "interestRateFrequencyType": { + "id": 3, + "code": "interestRateFrequency.periodFrequencyType.years", + "value": "Per year" + }, + "annualInterestRate": 24, + "amortizationType": { + "id": 1, + "code": "amortizationType.equal.installments", + "value": "Equal installments" + }, + "interestType": { + "id": 1, + "code": "interestType.flat", + "value": "Flat" + }, + "interestCalculationPeriodType": { + "id": 1, + "code": "interestCalculationPeriodType.same.as.repayment.period", + "value": "Same as repayment period" + }, + "transactionProcessingStrategyId": 2, + "timeline": { + "submittedOnDate": [ + 2012, + 4, + 3 + ], + "submittedByUsername": "admin", + "submittedByFirstname": "App", + "submittedByLastname": "Administrator", + "approvedOnDate": [ + 2012, + 4, + 3 + ], + "approvedByUsername": "admin", + "approvedByFirstname": "App", + "approvedByLastname": "Administrator", + "expectedDisbursementDate": [ + 2012, + 4, + 10 + ], + "actualDisbursementDate": [ + 2012, + 4, + 10 + ], + "disbursedByUsername": "admin", + "disbursedByFirstname": "App", + "disbursedByLastname": "Administrator", + "expectedMaturityDate": [ + 2013, + 4, + 10 + ] + }, + "summary": { + "currency": { + "code": "UGX", + "name": "Uganda Shilling", + "decimalPlaces": 2, + "displaySymbol": "USh", + "nameCode": "currency.UGX", + "displayLabel": "Uganda Shilling (USh)" + }, + "principalDisbursed": 1000000, + "principalPaid": 0, + "principalWrittenOff": 0, + "principalOutstanding": 1000000, + "principalOverdue": 833333.3, + "interestCharged": 240000, + "interestPaid": 0, + "interestWaived": 0, + "interestWrittenOff": 0, + "interestOutstanding": 240000, + "interestOverdue": 200000, + "feeChargesCharged": 18000, + "feeChargesDueAtDisbursementCharged": 0, + "feeChargesPaid": 0, + "feeChargesWaived": 0, + "feeChargesWrittenOff": 0, + "feeChargesOutstanding": 18000, + "feeChargesOverdue": 15000, + "penaltyChargesCharged": 0, + "penaltyChargesPaid": 0, + "penaltyChargesWaived": 0, + "penaltyChargesWrittenOff": 0, + "penaltyChargesOutstanding": 0, + "penaltyChargesOverdue": 0, + "totalExpectedRepayment": 1258000, + "totalRepayment": 0, + "totalExpectedCostOfLoan": 258000, + "totalCostOfLoan": 0, + "totalWaived": 0, + "totalWrittenOff": 0, + "totalOutstanding": 1258000, + "totalOverdue": 1048333.3, + "overdueSinceDate": [ + 2012, + 5, + 10 + ], + "linkedAccount":{ + "id":1, + "accountNo":"000000001" + }, + "disbursementDetails":[{"id":71,"expectedDisbursementDate":[2013,11,1],"principal":22000.000000,"approvedPrincipal":22000.000000}], + "fixedEmiAmount":1100.000000, + "maxOutstandingLoanBalance":35000, + "canDisburse":false, + "emiAmountVariations": [], + "inArrears": true, + "isNPA":false, + "overdueCharges": [ + { + "id": 20, + "name": "overdraft penality", + "active": true, + "penalty": true, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 3.000000, + "chargeTimeType": { + "id": 9, + "code": "chargeTimeType.overdueInstallment", + "value": "overdue fees" + }, + "chargeAppliesTo": { + "id": 1, + "code": "chargeAppliesTo.loan", + "value": "Loan" + }, + "chargeCalculationType": { + "id": 2, + "code": "chargeCalculationType.percent.of.amount", + "value": "% Amount" + }, + "chargePaymentMode": { + "id": 0, + "code": "chargepaymentmode.regular", + "value": "Regular" + }, + "feeInterval": 2, + "feeFrequency": { + "id": 1, + "code": "feeFrequencyperiodFrequencyType.weeks", + "value": "Weeks" + } + } + ] + } +} + +
    +
    + +   +
    +
    +

    Retrieve a Loan Transaction Details

    +

    Example Request:

    +
    self/loans/5/transactions/3
    +
    +
    + +GET https://DomainName/api/v1/self/loans/{loanId}/transactions/{transactionId} + + +{ + "id": 3, + "type": { + "id": 2, + "code": "loanTransactionType.repayment", + "value": "Repayment", + "disbursement": false, + "repaymentAtDisbursement": false, + "repayment": true, + "contra": false, + "waiveInterest": false, + "waiveCharges": false, + "writeOff": false, + "recoveryRepayment": false + }, + "date": [ + 2012, + 5, + 14 + ], + "manuallyReversed": false, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 559.88, + "interestPortion": 559.88 +} + +
    +
    + +   +
    +
    +

    List Loan Charges

    +

    Example Requests:

    +
    self/loans/1/charges
    +
    +
    +
    self/loans/1/charges?fields=name,amountOrPercentage
    +
    +
    + +GET https://DomainName/api/v1/self/loans/{loanId}/charges + + +[ + { + "id": 1, + "chargeId": 1, + "name": "Loan Processing fee", + "chargeTimeType": { + "id": 1, + "code": "chargeTimeType.disbursement", + "value": "Disbursement" + }, + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "percentage": 0, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 100, + "amountOrPercentage": 100, + "penalty": false + }, + { + "id": 7, + "chargeId": 2, + "name": "Collection Fee", + "chargeTimeType": { + "id": 2, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "dueDate": [ + 2013, + 3, + 29 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "percentage": 0, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 100, + "amountOrPercentage": 100, + "penalty": false + } +] + +
    +
    + +   +
    +
    +

    Retrieve a Loan Charge

    +

    Example Requests:

    +
    self/loans/1/charges/1
    +
    +
    +
    self/loans/1/charges/1?fields=name,amountOrPercentage
    +
    +
    + +GET https://DomainName/api/v1/self/loans/{loanId}/charges/{chargeId} + + +{ + "id": 1, + "chargeId": 1, + "name": "Loan Processing fee", + "chargeTimeType": { + "id": 1, + "code": "chargeTimeType.disbursement", + "value": "Disbursement" + }, + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "percentage": 0, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 100, + "amountOrPercentage": 100, + "penalty": false +} +
    +
    + +   +
    +
    +

    Retrieve a savings account:

    +

    Arguments

    +
    +
    associations
    +
    optional, a comma separated list of savings 'associations' (itemized below).
    +

    Associations are just extra pieces of data that you might or might not want to retrieve.

    +
    'transactions': Gets data related to transactions on the account e.g. ?associations=transactions
    +
    'charges':Savings Account charges data.
    +
    +

    Example Requests :

    +
    self/savingsaccounts/1
    +

    +
    self/savingsaccounts/1?associations=transactions
    +
    +
    + GET https://DomainName/api/v1/self/savingsaccounts/{accountId} + +{ + "id": 1, + "accountNo": "000000001", + "clientId": 1, + "clientName": "small business", + "savingsProductId": 1, + "savingsProductName": "Passbook Savings", + "fieldOfficerId": 0, + "status": { + "id": 100, + "code": "savingsAccountStatusType.submitted.and.pending.approval", + "value": "Submitted and pending approval", + "submittedAndPendingApproval": true, + "approved": false, + "rejected": false, + "withdrawnByApplicant": false, + "active": false, + "closed": false + }, + "timeline": { + "submittedOnDate": [ + 2013, + 3, + 1 + ] + }, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "nominalAnnualInterestRate": 5, + "interestCompoundingPeriodType": { + "id": 1, + "code": "savings.interest.period.savingsCompoundingInterestPeriodType.daily", + "value": "Daily" + }, + "interestPostingPeriodType": { + "id": 4, + "code": "savings.interest.posting.period.savingsPostingInterestPeriodType.monthly", + "value": "Monthly" + }, + "interestCalculationType": { + "id": 1, + "code": "savingsInterestCalculationType.dailybalance", + "value": "Daily Balance" + }, + "interestCalculationDaysInYearType": { + "id": 365, + "code": "savingsInterestCalculationDaysInYearType.days365", + "value": "365 Days" + }, + "summary": { + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "accountBalance": 0 + } +} + +
    +
    + +  +
    +
    +

    Retrieve Savings Account Transaction:

    +

    Example Requests:

    +
    self/savingsaccounts/1/transactions/1
    +
    +
    +
    + GET https://DomainName/api/v1/self/savingsaccounts/{accountId}/transactions/{transactionId} + + +{ + "id": 1, + "transactionType": { + "id": 2, + "code": "savingsAccountTransactionType.withdrawal", + "value": "Withdrawal", + "deposit": false, + "withdrawal": true, + "interestPosting": false, + "feeDeduction": false + }, + "accountId": 1, + "accountNo": "000000001", + "date": [ + 2013, + 8, + 7 + ], + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "inMultiplesOf": 0, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "paymentDetailData": { + "id": 62, + "paymentType": { + "id": 11, + "name": "cash" + }, + "accountNumber": "", + "checkNumber": "", + "routingCode": "", + "receiptNumber": "", + "bankNumber": "" + }, + "amount": 5000, + "runningBalance": 0, + "reversed": true +} + +
    +
    + +   +
    +
    +

    List Savings Charges

    +

    Example Requests:

    +
    self/savingsaccounts/1/charges
    +
    +
    self/savingsaccounts/1/charges?chargeStatus=inactive
    +
    +
    self/savingsaccounts/1/charges?fields=name,amountOrPercentage
    +
    +
    + +GET https://DomainName/api/v1/self/savingsaccounts/{accountId}/charges + + +[ + { + "id": 1, + "chargeId": 3, + "accountId": 57, + "name": "Savings account maintenance fee", + "chargeTimeType": { + "id": 1, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "percentage": 0, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 100, + "amountOrPercentage": 100, + "penalty": false + }, + { + "id": 2, + "chargeId": 4, + "accountId": 57, + "name": "Pass book Fee", + "chargeTimeType": { + "id": 2, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "dueDate": [ + 2013, + 3, + 29 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "percentage": 0, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 100, + "amountOrPercentage": 100, + "penalty": false + }, + { + "id": 9, + "chargeId": 4, + "accountId": 57, + "name": "Withdrawal fee percentage", + "chargeTimeType": { + "id": 5, + "code": "chargeTimeType.withdrawalFee", + "value": "Withdrawal Fee" + }, + "chargeCalculationType": { + "id": 2, + "code": "chargeCalculationType.percent.of.amount", + "value": "% Amount" + }, + "percentage": 0.25, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 0, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 0, + "amountOrPercentage": 0.25, + "penalty": false + }, + { + "id": 10, + "chargeId": 6, + "accountId": 57, + "name": "Annual fee - INR", + "chargeTimeType": { + "id": 6, + "code": "chargeTimeType.annualFee", + "value": "Annual Fee" + }, + "feeOnMonthDay": [ + 10, + 9 + ], + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "percentage": 0, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 50, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 50, + "amountOrPercentage": 50, + "penalty": false + } +] + +
    +
    + +   +
    +
    +

    Retrieve a Savings account Charge

    +

    Example Requests:

    +
    self/savingsaccounts/1/charges/5
    +
    +
    +
    self/savingsaccounts/1/charges/5?fields=name,amountOrPercentage
    +
    +
    + +GET https://DomainName/api/v1/self/savingsaccounts/{accountId}/charges/{savingsAccountChargeId} + + +{ + "id": 1, + "chargeId": 1, + "name": "Passbook fee", + "chargeTimeType": { + "id": 1, + "code": "chargeTimeType.specifiedDueDate", + "value": "Specified due date" + }, + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "percentage": 0, + "amountPercentageAppliedTo": 0, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 100, + "amountPaid": 0, + "amountWaived": 0, + "amountWrittenOff": 0, + "amountOutstanding": 100, + "amountOrPercentage": 100, + "penalty": false +} +
    +
    + +   +
    +
    +

    Retrieve Account Transfer Template

    +

    Returns list of loan/savings accounts that can be used for account transfer

    +

    Example Requests:

    +
    self/accounttransfers/template
    +
    +
    + GET https://DomainName/api/v1/self/accounttransfers/template + + +{ + "accountOptions": [ + { + "accountId": 1, + "accountNo": "00000001", + "accountType": { + "id": 2, + "code": "accountType.savings", + "value": "Savings Account" + }, + "clientId": 1, + "clientName": "ABC", + "officeId": 1, + "officeName": "HEAD OFFICE" + }, + { + "accountId": 5, + "accountNo": "00000005", + "accountType": { + "id": 1, + "code": "accountType.loan", + "value": "Loan Account" + }, + "clientId": 2, + "clientName": "XYZ", + "officeId": 3, + "officeName": "REGIONAL OFFICE" + } + ] +} + +
    +
    + +   +
    +
    +

    Create new Transfer

    +

    Ability to create new transfer of monetary funds from one account to another.

    +
    +
    + POST https://DomainName/api/v1/self/accounttransfers + POST self/accounttransfers/ +Content-Type: application/json +No Request Body: +{ +"fromOfficeId": 1, +"fromClientId": 1, +"fromAccountType": 2, +"fromAccountId": 1, +"toOfficeId": 1, +"toClientId": 1, +"toAccountType": 2, +"toAccountId": 2, +"dateFormat": "dd MMMM yyyy", +"locale": "en", +"transferDate": "01 August 2011", +"transferAmount": "112.45", +"transferDescription": "A description of the transfer" +} + + +{ + "savingsId": 1, + "resourceId": 1 +} + +
    +
    + + diff --git a/api-docs/apidocs.css b/api-docs/apidocs.css index 837a7ff7b..8690a5d90 100644 --- a/api-docs/apidocs.css +++ b/api-docs/apidocs.css @@ -192,7 +192,7 @@ body { .flybar-nav { display: block; float: left; - min-width: 90px; + min-width: 80px; position: relative; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/SearchParameters.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/SearchParameters.java index 762d260a9..b5d74f083 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/SearchParameters.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/SearchParameters.java @@ -34,21 +34,23 @@ public final class SearchParameters { private final Long provisioningEntryId ; private final Long productId ; private final Long categoryId ; + private final boolean isSelfUser; - public static SearchParameters from(final String sqlSearch, final Long officeId, final String externalId, final String name, + public static SearchParameters from(final String sqlSearch, final Long officeId, final String externalId, final String name, final String hierarchy) { final Long staffId = null; final String accountNo = null; final Long loanId = null; final Long savingsId = null; final Boolean orphansOnly = false; + final boolean isSelfUser = false; return new SearchParameters(sqlSearch, officeId, externalId, name, hierarchy, null, null, null, null, null, null, staffId, - accountNo, loanId, savingsId, orphansOnly); + accountNo, loanId, savingsId, orphansOnly, isSelfUser); } public static SearchParameters forClients(final String sqlSearch, final Long officeId, final String externalId, final String displayName, final String firstname, final String lastname, final String hierarchy, final Integer offset, - final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly) { + final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly, final boolean isSelfUser) { final Integer maxLimitAllowed = getCheckedLimit(limit); final Long staffId = null; @@ -57,7 +59,7 @@ public final class SearchParameters { final Long savingsId = null; return new SearchParameters(sqlSearch, officeId, externalId, displayName, hierarchy, firstname, lastname, offset, maxLimitAllowed, - orderBy, sortOrder, staffId, accountNo, loanId, savingsId, orphansOnly); + orderBy, sortOrder, staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser); } public static SearchParameters forGroups(final String sqlSearch, final Long officeId, final Long staffId, final String externalId, @@ -68,15 +70,17 @@ public final class SearchParameters { final String accountNo = null; final Long loanId = null; final Long savingsId = null; + final boolean isSelfUser = false; return new SearchParameters(sqlSearch, officeId, externalId, name, hierarchy, null, null, offset, maxLimitAllowed, orderBy, - sortOrder, staffId, accountNo, loanId, savingsId, orphansOnly); + sortOrder, staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser); } public static SearchParameters forOffices(final String orderBy, final String sortOrder) { final Boolean orphansOnly = false; + final boolean isSelfUser = false; return new SearchParameters(null, null, null, null, null, null, null, null, null, orderBy, sortOrder, null, null, null, null, - orphansOnly); + orphansOnly, isSelfUser); } public static SearchParameters forLoans(final String sqlSearch, final String externalId, final Integer offset, final Integer limit, @@ -87,9 +91,10 @@ public final class SearchParameters { final Long loanId = null; final Long savingsId = null; final Boolean orphansOnly = false; + final boolean isSelfUser = false; return new SearchParameters(sqlSearch, null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, - staffId, accountNo, loanId, savingsId, orphansOnly); + staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser); } public static SearchParameters forJournalEntries(final Long officeId, final Integer offset, final Integer limit, final String orderBy, @@ -98,9 +103,10 @@ public final class SearchParameters { final Integer maxLimitAllowed = getCheckedLimit(limit); final Long staffId = null; final Boolean orphansOnly = false; + final boolean isSelfUser = false; return new SearchParameters(null, officeId, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, - null, loanId, savingsId, orphansOnly); + null, loanId, savingsId, orphansOnly, isSelfUser); } public static SearchParameters forJournalEntries(final Long officeId, final Integer offset, final Integer limit, final String orderBy, @@ -120,9 +126,10 @@ public final class SearchParameters { final Long loanId = null; final Long savingsId = null; final Boolean orphansOnly = false; + final boolean isSelfUser = false; return new SearchParameters(null, null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null, - loanId, savingsId, orphansOnly); + loanId, savingsId, orphansOnly, isSelfUser); } public static SearchParameters forPagination(final Integer offset, final Integer limit) { @@ -134,9 +141,10 @@ public final class SearchParameters { final Boolean orphansOnly = false; final String orderBy = null; final String sortOrder = null; + final boolean isSelfUser = false; return new SearchParameters(null, null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null, - loanId, savingsId, orphansOnly); + loanId, savingsId, orphansOnly, isSelfUser); } public final static SearchParameters forProvisioningEntries(final Long provisioningEntryId, final Long officeId, final Long productId, @@ -153,9 +161,10 @@ public final class SearchParameters { final Long loanId = null; final Long savingsId = null; final Boolean orphansOnly = false; + final boolean isSelfUser = false; return new SearchParameters(sqlSearch, null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, - staffId, accountNo, loanId, savingsId, orphansOnly); + staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser); } public static SearchParameters forAccountTransfer(final String sqlSearch, final String externalId, final Integer offset, @@ -167,15 +176,16 @@ public final class SearchParameters { final Long loanId = null; final Long savingsId = null; final Boolean orphansOnly = false; + final boolean isSelfUser = false; return new SearchParameters(sqlSearch, null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, - staffId, accountNo, loanId, savingsId, orphansOnly); + staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser); } private SearchParameters(final String sqlSearch, final Long officeId, final String externalId, final String name, final String hierarchy, final String firstname, final String lastname, final Integer offset, final Integer limit, final String orderBy, final String sortOrder, final Long staffId, final String accountNo, final Long loanId, - final Long savingsId, final Boolean orphansOnly) { + final Long savingsId, final Boolean orphansOnly, boolean isSelfUser) { this.sqlSearch = sqlSearch; this.officeId = officeId; this.externalId = externalId; @@ -196,6 +206,7 @@ public final class SearchParameters { this.provisioningEntryId = null ; this.productId = null ; this.categoryId = null ; + this.isSelfUser = isSelfUser; } @@ -221,6 +232,7 @@ public final class SearchParameters { this.provisioningEntryId = provisioningEntryId ; this.productId = productId ; this.categoryId = categoryId ; + this.isSelfUser = false; } @@ -248,6 +260,7 @@ public final class SearchParameters { this.provisioningEntryId = null ; this.productId = null ; this.categoryId = null ; + this.isSelfUser = false; } public boolean isOrderByRequested() { @@ -397,4 +410,9 @@ public final class SearchParameters { public boolean isCategoryIdPassed() { return this.categoryId != null && this.categoryId != 0 ; } + + public boolean isSelfUser() { + return this.isSelfUser; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java index a95b9c96d..09f4862fe 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java @@ -24,11 +24,15 @@ import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.security.data.PlatformRequestLog; import org.mifosplatform.infrastructure.security.exception.InvalidTenantIdentiferException; import org.mifosplatform.infrastructure.security.service.BasicAuthTenantDetailsService; +import org.mifosplatform.useradministration.domain.AppUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @@ -143,4 +147,22 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil logger.info(this.toApiJsonSerializer.serialize(log)); } } + + @Override + protected void onSuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, Authentication authResult) + throws IOException { + super.onSuccessfulAuthentication(request, response, authResult); + AppUser user = (AppUser) authResult.getPrincipal(); + + String pathURL = request.getRequestURI(); + boolean isSelfServiceRequest = (pathURL != null && pathURL.contains("/self/")); + + boolean notAllowed = ((isSelfServiceRequest && !user.isSelfServiceUser()) + ||(!isSelfServiceRequest && user.isSelfServiceUser())); + + if(notAllowed){ + throw new BadCredentialsException("User not authorised to use the requested resource."); + } + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java index 017776240..91004c0b7 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareTenantIdentifierFilter.java @@ -88,7 +88,7 @@ public class TenantAwareTenantIdentifierFilter extends GenericFilterBean { response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); final String reqHead = request.getHeader("Access-Control-Request-Headers"); - if (null != reqHead && !reqHead.equals(null)) { + if (null != reqHead && !reqHead.isEmpty()) { response.setHeader("Access-Control-Allow-Headers", reqHead); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/vote/SelfServiceUserAccessVote.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/vote/SelfServiceUserAccessVote.java new file mode 100644 index 000000000..179d7d07f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/vote/SelfServiceUserAccessVote.java @@ -0,0 +1,48 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.security.vote; + +import java.util.Collection; + +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; + +public class SelfServiceUserAccessVote implements AccessDecisionVoter { + + @Override + public boolean supports(@SuppressWarnings("unused") ConfigAttribute attribute) { + // This implementation supports any attribute, because it does not rely on it. + return true; + } + + @Override + public boolean supports(Class clazz) { + return FilterInvocation.class.isAssignableFrom(clazz); + } + + @Override + public int vote(final Authentication authentication, final FilterInvocation fi, + @SuppressWarnings("unused") final Collection attributes) { + if(!"OPTIONS".equalsIgnoreCase(fi.getHttpRequest().getMethod())){ + AppUser user = (AppUser) authentication.getPrincipal(); + + String pathURL = fi.getRequestUrl(); + boolean isSelfServiceRequest = (pathURL != null && pathURL.contains("/self/")); + + boolean notAllowed = ((isSelfServiceRequest && !user.isSelfServiceUser()) + ||(!isSelfServiceRequest && user.isSelfServiceUser())); + + if(notAllowed){ + return ACCESS_DENIED; + } + } + return ACCESS_GRANTED; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java index 948db0dc6..2ec85a6c0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersReadPlatformServiceImpl.java @@ -75,7 +75,7 @@ public class AccountTransfersReadPlatformServiceImpl implements AccountTransfers final Integer mostRelevantFromAccountType = fromAccountType; final Collection fromAccountTypeOptions = Arrays.asList(savingsAccountType, loanAccountType); final Collection toAccountTypeOptions; - if (mostRelevantFromAccountType == 1) { + if (mostRelevantFromAccountType != null && mostRelevantFromAccountType == 1) { // overpaid loan amt transfer to savings account toAccountTypeOptions = Arrays.asList(savingsAccountType); } else { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java index b2e9d4f17..19bbd3787 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientsApiResource.java @@ -117,10 +117,22 @@ public class ClientsApiResource { @QueryParam("orderBy") final String orderBy, @QueryParam("sortOrder") final String sortOrder, @QueryParam("orphansOnly") final Boolean orphansOnly) { + return this.retrieveAll(uriInfo, sqlSearch, officeId, externalId, displayName, firstname, + lastname, hierarchy, offset, limit, orderBy, sortOrder, orphansOnly, false); + } + + public String retrieveAll(final UriInfo uriInfo, final String sqlSearch, + final Long officeId, final String externalId, + final String displayName, final String firstname, + final String lastname, final String hierarchy, + final Integer offset, final Integer limit, + final String orderBy, final String sortOrder, + final Boolean orphansOnly, final boolean isSelfUser) { + this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME); final SearchParameters searchParameters = SearchParameters.forClients(sqlSearch, officeId, externalId, displayName, firstname, - lastname, hierarchy, offset, limit, orderBy, sortOrder, orphansOnly); + lastname, hierarchy, offset, limit, orderBy, sortOrder, orphansOnly, isSelfUser); final Page clientData = this.clientReadPlatformService.retrieveAll(searchParameters); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java index fbbd18f6a..1fd90745a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -113,6 +113,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String userOfficeHierarchy = this.context.officeHierarchy(); final String underHierarchySearchString = userOfficeHierarchy + "%"; + final String appUserID = String.valueOf(context.authenticatedUser().getId()); // if (searchParameters.isScopedByOfficeHierarchy()) { // this.context.validateAccessRights(searchParameters.getHierarchy()); @@ -123,6 +124,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService sqlBuilder.append("select SQL_CALC_FOUND_ROWS "); sqlBuilder.append(this.clientMapper.schema()); sqlBuilder.append(" where (o.hierarchy like ? or transferToOffice.hierarchy like ?) "); + + if(searchParameters.isSelfUser()){ + sqlBuilder.append(" and c.id in (select umap.client_id from m_selfservice_user_client_mapping as umap where umap.appuser_id = ? ) "); + } final String extraCriteria = buildSqlStringFromClientCriteria(searchParameters); @@ -146,8 +151,11 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService } final String sqlCountRows = "SELECT FOUND_ROWS()"; - return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(), new Object[] { - underHierarchySearchString, underHierarchySearchString }, this.clientMapper); + Object[] params = new Object[] {underHierarchySearchString, underHierarchySearchString }; + if(searchParameters.isSelfUser()){ + params = new Object[] {underHierarchySearchString, underHierarchySearchString, appUserID }; + } + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(), params, this.clientMapper); } private String buildSqlStringFromClientCriteria(final SearchParameters searchParameters) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/api/SelfAccountTransferApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/api/SelfAccountTransferApiResource.java new file mode 100644 index 000000000..e8b4d2c0c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/api/SelfAccountTransferApiResource.java @@ -0,0 +1,85 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.account.api; + +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; +import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.account.api.AccountTransfersApiResource; +import org.mifosplatform.portfolio.self.account.data.SelfAccountTemplateData; +import org.mifosplatform.portfolio.self.account.data.SelfAccountTransferData; +import org.mifosplatform.portfolio.self.account.data.SelfAccountTransferDataValidator; +import org.mifosplatform.portfolio.self.account.service.SelfAccountTransferReadService; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/self/accounttransfers") +@Component +@Scope("singleton") +public class SelfAccountTransferApiResource { + + private final PlatformSecurityContext context; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final AccountTransfersApiResource accountTransfersApiResource; + private final SelfAccountTransferReadService selfAccountTransferReadService; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final SelfAccountTransferDataValidator dataValidator; + + @Autowired + public SelfAccountTransferApiResource( + final PlatformSecurityContext context, + final DefaultToApiJsonSerializer toApiJsonSerializer, + final AccountTransfersApiResource accountTransfersApiResource, + final SelfAccountTransferReadService selfAccountTransferReadService, + final ApiRequestParameterHelper apiRequestParameterHelper, + final SelfAccountTransferDataValidator dataValidator) { + this.context = context; + this.toApiJsonSerializer = toApiJsonSerializer; + this.accountTransfersApiResource = accountTransfersApiResource; + this.selfAccountTransferReadService = selfAccountTransferReadService; + this.apiRequestParameterHelper = apiRequestParameterHelper; + this.dataValidator = dataValidator; + } + + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String template(@Context final UriInfo uriInfo) { + + AppUser user = this.context.authenticatedUser(); + Collection templateData = this.selfAccountTransferReadService + .retrieveSelfAccountTemplateData(user); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper + .process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, + new SelfAccountTransferData(templateData)); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String create(final String apiRequestBodyAsJson) { + this.dataValidator.validateCreate(apiRequestBodyAsJson); + return this.accountTransfersApiResource.create(apiRequestBodyAsJson); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTemplateData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTemplateData.java new file mode 100644 index 000000000..a16b5802e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTemplateData.java @@ -0,0 +1,93 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.account.data; + +import org.apache.commons.lang.builder.CompareToBuilder; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; +import org.mifosplatform.portfolio.account.service.AccountTransferEnumerations; + +@SuppressWarnings("unused") +public class SelfAccountTemplateData implements + Comparable { + + private final Long accountId; + private final String accountNo; + private final EnumOptionData accountType; + private final Long clientId; + private final String clientName; + private final Long officeId; + private final String officeName; + + public SelfAccountTemplateData(final Long accountId, + final String accountNo, final Integer accountType, + final Long clientId, final String clientName, final Long officeId, + final String officeName) { + this.accountId = accountId; + this.accountNo = accountNo; + this.accountType = AccountTransferEnumerations.accountType(accountType); + this.clientId = clientId; + this.clientName = clientName; + this.officeId = officeId; + this.officeName = officeName; + } + + public SelfAccountTemplateData(final Long accountId, + final Integer accountType, final Long clientId, final Long officeId) { + this.accountId = accountId; + this.accountNo = null; + this.accountType = AccountTransferEnumerations.accountType(accountType); + this.clientId = clientId; + this.clientName = null; + this.officeId = officeId; + this.officeName = null; + } + + @Override + public int compareTo(final SelfAccountTemplateData obj) { + if (obj == null) { + return -1; + } + return new CompareToBuilder() // + .append(this.accountId, obj.accountId) // + .append(this.accountType.getValue(), obj.accountType.getValue()) // + .append(this.clientId, obj.clientId) // + .append(this.officeId, obj.officeId) // + .toComparison(); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj.getClass() != getClass()) { + return false; + } + final SelfAccountTemplateData rhs = (SelfAccountTemplateData) obj; + return new EqualsBuilder() // + .append(this.accountId, rhs.accountId) // + .append(this.accountType.getValue(), rhs.accountType.getValue()) // + .append(this.clientId, rhs.clientId) // + .append(this.officeId, rhs.officeId) // + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) // + .append(this.accountId) // + .append(this.accountType.getValue()) // + .append(this.clientId) // + .append(this.officeId) // + .toHashCode(); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTransferData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTransferData.java new file mode 100644 index 000000000..e07a22db5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTransferData.java @@ -0,0 +1,20 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.account.data; + +import java.util.Collection; + +@SuppressWarnings("unused") +public class SelfAccountTransferData { + + private final Collection accountOptions; + + public SelfAccountTransferData( + final Collection accountOptions) { + this.accountOptions = accountOptions; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTransferDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTransferDataValidator.java new file mode 100644 index 000000000..b89e5cf5c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/data/SelfAccountTransferDataValidator.java @@ -0,0 +1,180 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.account.data; + +import static org.mifosplatform.portfolio.account.AccountDetailConstants.fromAccountIdParamName; +import static org.mifosplatform.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; +import static org.mifosplatform.portfolio.account.AccountDetailConstants.fromClientIdParamName; +import static org.mifosplatform.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; +import static org.mifosplatform.portfolio.account.AccountDetailConstants.toAccountIdParamName; +import static org.mifosplatform.portfolio.account.AccountDetailConstants.toAccountTypeParamName; +import static org.mifosplatform.portfolio.account.AccountDetailConstants.toClientIdParamName; +import static org.mifosplatform.portfolio.account.AccountDetailConstants.toOfficeIdParamName; +import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstants.ACCOUNT_TRANSFER_RESOURCE_NAME; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.self.account.service.SelfAccountTransferReadService; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonElement; + +@Component +public class SelfAccountTransferDataValidator { + + private final PlatformSecurityContext context; + private final SelfAccountTransferReadService selfAccountTransferReadService; + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public SelfAccountTransferDataValidator( + final PlatformSecurityContext context, + final SelfAccountTransferReadService selfAccountTransferReadService, + final FromJsonHelper fromApiJsonHelper) { + this.context = context; + this.selfAccountTransferReadService = selfAccountTransferReadService; + this.fromApiJsonHelper = fromApiJsonHelper; + } + + public void validateCreate(String apiRequestBodyAsJson) { + if (StringUtils.isBlank(apiRequestBodyAsJson)) { + throw new InvalidJsonException(); + } + + JsonElement element = this.fromApiJsonHelper + .parse(apiRequestBodyAsJson); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder( + dataValidationErrors).resource(ACCOUNT_TRANSFER_RESOURCE_NAME); + + final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed( + fromOfficeIdParamName, element); + baseDataValidator.reset().parameter(fromOfficeIdParamName) + .value(fromOfficeId).notNull().integerGreaterThanZero(); + + final Long fromClientId = this.fromApiJsonHelper.extractLongNamed( + fromClientIdParamName, element); + baseDataValidator.reset().parameter(fromClientIdParamName) + .value(fromClientId).notNull().integerGreaterThanZero(); + + final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed( + fromAccountIdParamName, element); + baseDataValidator.reset().parameter(fromAccountIdParamName) + .value(fromAccountId).notNull().integerGreaterThanZero(); + + final Integer fromAccountType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(fromAccountTypeParamName, + element); + baseDataValidator.reset().parameter(fromAccountTypeParamName) + .value(fromAccountType).notNull() + .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2)); + + final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed( + toOfficeIdParamName, element); + baseDataValidator.reset().parameter(toOfficeIdParamName) + .value(toOfficeId).notNull().integerGreaterThanZero(); + + final Long toClientId = this.fromApiJsonHelper.extractLongNamed( + toClientIdParamName, element); + baseDataValidator.reset().parameter(toClientIdParamName) + .value(toClientId).notNull().integerGreaterThanZero(); + + final Long toAccountId = this.fromApiJsonHelper.extractLongNamed( + toAccountIdParamName, element); + baseDataValidator.reset().parameter(toAccountIdParamName) + .value(toAccountId).notNull().integerGreaterThanZero(); + + final Integer toAccountType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(toAccountTypeParamName, element); + baseDataValidator.reset().parameter(toAccountTypeParamName) + .value(toAccountType).notNull() + .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2)); + + if (fromAccountType != null && fromAccountType == 1 + && toAccountType != null && toAccountType == 1) { + baseDataValidator + .reset() + .failWithCode("loan.to.loan.transfer.not.allowed", + "Cannot transfer from Loan account to another Loan account."); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + SelfAccountTemplateData fromAccount = new SelfAccountTemplateData( + fromAccountId, fromAccountType, fromClientId, fromOfficeId); + SelfAccountTemplateData toAccount = new SelfAccountTemplateData( + toAccountId, toAccountType, toClientId, toOfficeId); + + validateSelfUserAccounts(fromAccount, toAccount, baseDataValidator); + throwExceptionIfValidationWarningsExist(dataValidationErrors); + + } + + private void validateSelfUserAccounts( + final SelfAccountTemplateData fromAccount, + final SelfAccountTemplateData toAccount, + final DataValidatorBuilder baseDataValidator) { + AppUser user = this.context.authenticatedUser(); + Collection userValidAccounts = this.selfAccountTransferReadService + .retrieveSelfAccountTemplateData(user); + + boolean validFromAccount = false; + for (SelfAccountTemplateData validAccount : userValidAccounts) { + if (validAccount.equals(fromAccount)) { + validFromAccount = true; + break; + } + } + + boolean validToAccount = false; + for (SelfAccountTemplateData validAccount : userValidAccounts) { + if (validAccount.equals(toAccount)) { + validToAccount = true; + break; + } + } + + if (!validFromAccount) { + baseDataValidator + .reset() + .failWithCode("invalid.from.account.details", + "Source account details doesn't match with valid user account details."); + } + + if (!validToAccount) { + baseDataValidator + .reset() + .failWithCode("invalid.to.account.details", + "Destination account details doesn't match with valid user account details."); + } + + if (fromAccount.equals(toAccount)) { + baseDataValidator.reset().failWithCode( + "same.from.to.account.details", + "Source and Destination account details are same."); + } + + } + + private void throwExceptionIfValidationWarningsExist( + final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/service/SelfAccountTransferReadService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/service/SelfAccountTransferReadService.java new file mode 100644 index 000000000..c8731f417 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/service/SelfAccountTransferReadService.java @@ -0,0 +1,18 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.account.service; + +import java.util.Collection; + +import org.mifosplatform.portfolio.self.account.data.SelfAccountTemplateData; +import org.mifosplatform.useradministration.domain.AppUser; + +public interface SelfAccountTransferReadService { + + Collection retrieveSelfAccountTemplateData( + AppUser user); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/service/SelfAccountTransferReadServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/service/SelfAccountTransferReadServiceImpl.java new file mode 100644 index 000000000..4335a24d9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/account/service/SelfAccountTransferReadServiceImpl.java @@ -0,0 +1,89 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.account.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; + +import org.mifosplatform.infrastructure.core.service.RoutingDataSource; +import org.mifosplatform.portfolio.self.account.data.SelfAccountTemplateData; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +public class SelfAccountTransferReadServiceImpl implements + SelfAccountTransferReadService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public SelfAccountTransferReadServiceImpl(final RoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public Collection retrieveSelfAccountTemplateData( + AppUser user) { + SelfAccountTemplateMapper mapper = new SelfAccountTemplateMapper(); + StringBuffer sql = new StringBuffer() + .append("select s.id as accountId, ") + .append("s.account_no as accountNo, ") + .append("2 as accountType, ") + .append("c.id as clientId, ") + .append("c.display_name as clientName, ") + .append("o.id as officeId, ") + .append("o.name as officeName ") + .append("from m_appuser as u ") + .append("inner join m_selfservice_user_client_mapping as map on u.id = map.appuser_id ") + .append("inner join m_client as c on map.client_id = c.id ") + .append("inner join m_office as o on c.office_id = o.id ") + .append("inner join m_savings_account as s on s.client_id = c.id ") + .append("where u.id = ? ") + .append("and s.status_enum = 300 ") + .append("union ") + .append("select l.id as accountId, ") + .append("l.account_no as accountNo, ") + .append("1 as accountType, ") + .append("c.id as clientId, ") + .append("c.display_name as clientName, ") + .append("o.id as officeId, ") + .append("o.name as officeName ") + .append("from m_appuser as u ") + .append("inner join m_selfservice_user_client_mapping as map on u.id = map.appuser_id ") + .append("inner join m_client as c on map.client_id = c.id ") + .append("inner join m_office as o on c.office_id = o.id ") + .append("inner join m_loan as l on l.client_id = c.id ") + .append("where u.id = ? ") + .append("and l.loan_status_id = 300 "); + return this.jdbcTemplate.query(sql.toString(), mapper, new Object[] { + user.getId(), user.getId() }); + } + + private final class SelfAccountTemplateMapper implements + RowMapper { + + @Override + public SelfAccountTemplateData mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + final Long accountId = rs.getLong("accountId"); + final String accountNo = rs.getString("accountNo"); + final Integer accountType = rs.getInt("accountType"); + final Long clientId = rs.getLong("clientId"); + final String clientName = rs.getString("clientName"); + final Long officeId = rs.getLong("officeId"); + final String officeName = rs.getString("officeName"); + + return new SelfAccountTemplateData(accountId, accountNo, + accountType, clientId, clientName, officeId, officeName); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/api/SelfClientsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/api/SelfClientsApiResource.java new file mode 100644 index 000000000..521c5cd98 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/api/SelfClientsApiResource.java @@ -0,0 +1,208 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.client.api; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.infrastructure.documentmanagement.api.ImagesApiResource; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.client.api.ClientApiConstants; +import org.mifosplatform.portfolio.client.api.ClientChargesApiResource; +import org.mifosplatform.portfolio.client.api.ClientTransactionsApiResource; +import org.mifosplatform.portfolio.client.api.ClientsApiResource; +import org.mifosplatform.portfolio.client.exception.ClientNotFoundException; +import org.mifosplatform.portfolio.self.client.data.SelfClientDataValidator; +import org.mifosplatform.portfolio.self.client.service.AppuserClientMapperReadService; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/self/clients") +@Component +@Scope("singleton") +public class SelfClientsApiResource { + + private final PlatformSecurityContext context; + private final ClientsApiResource clientApiResource; + private final ImagesApiResource imagesApiResource; + private final ClientChargesApiResource clientChargesApiResource; + private final ClientTransactionsApiResource clientTransactionsApiResource; + private final AppuserClientMapperReadService appUserClientMapperReadService; + private final SelfClientDataValidator dataValidator; + + @Autowired + public SelfClientsApiResource( + final PlatformSecurityContext context, + final ClientsApiResource clientApiResource, + final ImagesApiResource imagesApiResource, + final ClientChargesApiResource clientChargesApiResource, + final ClientTransactionsApiResource clientTransactionsApiResource, + final AppuserClientMapperReadService appUserClientMapperReadService, + final SelfClientDataValidator dataValidator) { + this.context = context; + this.clientApiResource = clientApiResource; + this.imagesApiResource = imagesApiResource; + this.clientChargesApiResource = clientChargesApiResource; + this.clientTransactionsApiResource = clientTransactionsApiResource; + this.appUserClientMapperReadService = appUserClientMapperReadService; + this.dataValidator = dataValidator; + } + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAll(@Context final UriInfo uriInfo, + @QueryParam("displayName") final String displayName, + @QueryParam("firstName") final String firstname, + @QueryParam("lastName") final String lastname, + @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit, + @QueryParam("orderBy") final String orderBy, + @QueryParam("sortOrder") final String sortOrder) { + + final String sqlSearch = null; + final Long officeId = null; + final String externalId = null; + final String hierarchy = null; + final Boolean orphansOnly = null; + return this.clientApiResource.retrieveAll(uriInfo, sqlSearch, officeId, + externalId, displayName, firstname, lastname, hierarchy, + offset, limit, orderBy, sortOrder, orphansOnly, true); + } + + @GET + @Path("{clientId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveOne(@PathParam("clientId") final Long clientId, + @Context final UriInfo uriInfo) { + + this.dataValidator.validateRetrieveOne(uriInfo); + + validateAppuserClientsMapping(clientId); + + final boolean staffInSelectedOfficeOnly = false; + return this.clientApiResource.retrieveOne(clientId, uriInfo, + staffInSelectedOfficeOnly); + } + + @GET + @Path("{clientId}/accounts") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAssociatedAccounts( + @PathParam("clientId") final Long clientId, + @Context final UriInfo uriInfo) { + + validateAppuserClientsMapping(clientId); + + return this.clientApiResource.retrieveAssociatedAccounts(clientId, + uriInfo); + } + + @GET + @Path("{clientId}/images") + @Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML, + MediaType.APPLICATION_JSON }) + @Produces({ MediaType.TEXT_PLAIN }) + public Response retrieveImage(@PathParam("clientId") final Long clientId, + @QueryParam("maxWidth") final Integer maxWidth, + @QueryParam("maxHeight") final Integer maxHeight, + @QueryParam("output") final String output) { + + validateAppuserClientsMapping(clientId); + + return this.imagesApiResource.retrieveImage("clients", clientId, + maxWidth, maxHeight, output); + } + + @GET + @Path("{clientId}/charges") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAllClientCharges( + @PathParam("clientId") final Long clientId, + @DefaultValue(ClientApiConstants.CLIENT_CHARGE_QUERY_PARAM_STATUS_VALUE_ALL) @QueryParam(ClientApiConstants.CLIENT_CHARGE_QUERY_PARAM_STATUS) final String chargeStatus, + @QueryParam("pendingPayment") final Boolean pendingPayment, + @Context final UriInfo uriInfo, + @QueryParam("limit") final Integer limit, + @QueryParam("offset") final Integer offset) { + + validateAppuserClientsMapping(clientId); + + return this.clientChargesApiResource.retrieveAllClientCharges(clientId, + chargeStatus, pendingPayment, uriInfo, limit, offset); + } + + @GET + @Path("{clientId}/charges/{chargeId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveClientCharge( + @PathParam("clientId") final Long clientId, + @PathParam("chargeId") final Long chargeId, + @Context final UriInfo uriInfo) { + + this.dataValidator.validateClientCharges(uriInfo); + + validateAppuserClientsMapping(clientId); + + return this.clientChargesApiResource.retrieveClientCharge(clientId, + chargeId, uriInfo); + } + + @GET + @Path("{clientId}/transactions") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAllClientTransactions( + @PathParam("clientId") final Long clientId, + @Context final UriInfo uriInfo, + @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit) { + + validateAppuserClientsMapping(clientId); + + return this.clientTransactionsApiResource + .retrieveAllClientTransactions(clientId, uriInfo, offset, limit); + } + + @GET + @Path("{clientId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveClientTransaction( + @PathParam("clientId") final Long clientId, + @PathParam("transactionId") final Long transactionId, + @Context final UriInfo uriInfo) { + + validateAppuserClientsMapping(clientId); + + return this.clientTransactionsApiResource.retrieveClientTransaction( + clientId, transactionId, uriInfo); + } + + private void validateAppuserClientsMapping(final Long clientId) { + AppUser user = this.context.authenticatedUser(); + final boolean mappedClientId = this.appUserClientMapperReadService + .isClientMappedToUser(clientId, user.getId()); + if (!mappedClientId) { + throw new ClientNotFoundException(clientId); + } + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/data/SelfClientDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/data/SelfClientDataValidator.java new file mode 100644 index 000000000..0a6fae013 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/data/SelfClientDataValidator.java @@ -0,0 +1,64 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.client.data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; +import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException; +import org.springframework.stereotype.Component; + +@Component +public class SelfClientDataValidator { + + private static final Set allowedChargesAssociationParameters = new HashSet<>( + Arrays.asList("transactions")); + + public void validateClientCharges(final UriInfo uriInfo) { + List unsupportedParams = new ArrayList<>(); + + Set associationParameters = ApiParameterHelper + .extractAssociationsForResponseIfProvided(uriInfo + .getQueryParameters()); + if (!associationParameters.isEmpty()) { + associationParameters + .removeAll(allowedChargesAssociationParameters); + if (!associationParameters.isEmpty()) { + unsupportedParams.addAll(associationParameters); + } + } + + if (uriInfo.getQueryParameters().getFirst("exclude") != null) { + unsupportedParams.add("exclude"); + } + + if (unsupportedParams.size() > 0) { + throw new UnsupportedParameterException(unsupportedParams); + } + } + + public void validateRetrieveOne(final UriInfo uriInfo) { + List unsupportedParams = new ArrayList<>(); + + final boolean templateRequest = ApiParameterHelper.template(uriInfo + .getQueryParameters()); + if (templateRequest) { + unsupportedParams.add("template"); + } + + if (unsupportedParams.size() > 0) { + throw new UnsupportedParameterException(unsupportedParams); + } + + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/service/AppuserClientMapperReadService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/service/AppuserClientMapperReadService.java new file mode 100644 index 000000000..456a61d6b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/service/AppuserClientMapperReadService.java @@ -0,0 +1,12 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.client.service; + +public interface AppuserClientMapperReadService { + + public Boolean isClientMappedToUser(Long clientId, Long appUserId); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/service/AppuserClientMapperReadServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/service/AppuserClientMapperReadServiceImpl.java new file mode 100644 index 000000000..f900c75f5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/client/service/AppuserClientMapperReadServiceImpl.java @@ -0,0 +1,33 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.client.service; + +import org.mifosplatform.infrastructure.core.service.RoutingDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +@Service +public class AppuserClientMapperReadServiceImpl implements + AppuserClientMapperReadService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public AppuserClientMapperReadServiceImpl(final RoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public Boolean isClientMappedToUser(Long clientId, Long appUserId) { + return this.jdbcTemplate + .queryForObject( + "select case when (count(*) > 0) then true else false end " + + " from m_selfservice_user_client_mapping where client_id = ? and appuser_id = ?", + new Object[] { clientId, appUserId }, Boolean.class); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/api/SelfLoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/api/SelfLoansApiResource.java new file mode 100644 index 000000000..47b047047 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/api/SelfLoansApiResource.java @@ -0,0 +1,124 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.loanaccount.api; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.loanaccount.api.LoanChargesApiResource; +import org.mifosplatform.portfolio.loanaccount.api.LoanTransactionsApiResource; +import org.mifosplatform.portfolio.loanaccount.api.LoansApiResource; +import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; +import org.mifosplatform.portfolio.self.loanaccount.data.SelfLoansDataValidator; +import org.mifosplatform.portfolio.self.loanaccount.service.AppuserLoansMapperReadService; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/self/loans") +@Component +@Scope("singleton") +public class SelfLoansApiResource { + + private final PlatformSecurityContext context; + private final LoansApiResource loansApiResource; + private final LoanTransactionsApiResource loanTransactionsApiResource; + private final LoanChargesApiResource loanChargesApiResource; + private final AppuserLoansMapperReadService appuserLoansMapperReadService; + private final SelfLoansDataValidator dataValidator; + + @Autowired + public SelfLoansApiResource(final PlatformSecurityContext context, + final LoansApiResource loansApiResource, + final LoanTransactionsApiResource loanTransactionsApiResource, + final LoanChargesApiResource loanChargesApiResource, + final AppuserLoansMapperReadService appuserLoansMapperReadService, + final SelfLoansDataValidator dataValidator) { + this.context = context; + this.loansApiResource = loansApiResource; + this.loanTransactionsApiResource = loanTransactionsApiResource; + this.loanChargesApiResource = loanChargesApiResource; + this.appuserLoansMapperReadService = appuserLoansMapperReadService; + this.dataValidator = dataValidator; + } + + @GET + @Path("{loanId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveLoan(@PathParam("loanId") final Long loanId, + @Context final UriInfo uriInfo) { + + this.dataValidator.validateRetrieveLoan(uriInfo); + + validateAppuserLoanMapping(loanId); + + final boolean staffInSelectedOfficeOnly = false; + return this.loansApiResource.retrieveLoan(loanId, + staffInSelectedOfficeOnly, uriInfo); + } + + @GET + @Path("{loanId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveTransaction(@PathParam("loanId") final Long loanId, + @PathParam("transactionId") final Long transactionId, + @Context final UriInfo uriInfo) { + + this.dataValidator.validateRetrieveTransaction(uriInfo); + + validateAppuserLoanMapping(loanId); + + return this.loanTransactionsApiResource.retrieveTransaction(loanId, + transactionId, uriInfo); + } + + @GET + @Path("{loanId}/charges") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAllLoanCharges( + @PathParam("loanId") final Long loanId, + @Context final UriInfo uriInfo) { + + validateAppuserLoanMapping(loanId); + + return this.loanChargesApiResource.retrieveAllLoanCharges(loanId, + uriInfo); + } + + @GET + @Path("{loanId}/charges/{chargeId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveLoanCharge(@PathParam("loanId") final Long loanId, + @PathParam("chargeId") final Long loanChargeId, + @Context final UriInfo uriInfo) { + + validateAppuserLoanMapping(loanId); + + return this.retrieveLoanCharge(loanId, loanChargeId, uriInfo); + } + + private void validateAppuserLoanMapping(final Long loanId) { + AppUser user = this.context.authenticatedUser(); + final boolean isLoanMappedToUser = this.appuserLoansMapperReadService + .isLoanMappedToUser(loanId, user.getId()); + if (!isLoanMappedToUser) { + throw new LoanNotFoundException(loanId); + } + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/data/SelfLoansDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/data/SelfLoansDataValidator.java new file mode 100644 index 000000000..88c8a1547 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/data/SelfLoansDataValidator.java @@ -0,0 +1,74 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.loanaccount.data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; +import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException; +import org.springframework.stereotype.Component; + +@Component +public class SelfLoansDataValidator { + private static final Set allowedAssociationParameters = new HashSet<>( + Arrays.asList("repaymentSchedule", "futureSchedule", + "originalSchedule", "transactions", "charges", + "guarantors", "collateral", "linkedAccount", + "multiDisburseDetails")); + + public void validateRetrieveLoan(final UriInfo uriInfo) { + List unsupportedParams = new ArrayList<>(); + + validateTemplate(uriInfo, unsupportedParams); + + Set associationParameters = ApiParameterHelper + .extractAssociationsForResponseIfProvided(uriInfo + .getQueryParameters()); + if (!associationParameters.isEmpty()) { + associationParameters.removeAll(allowedAssociationParameters); + if (!associationParameters.isEmpty()) { + unsupportedParams.addAll(associationParameters); + } + } + + if (uriInfo.getQueryParameters().getFirst("exclude") != null) { + unsupportedParams.add("exclude"); + } + + throwExceptionIfReqd(unsupportedParams); + } + + public void validateRetrieveTransaction(UriInfo uriInfo) { + List unsupportedParams = new ArrayList<>(); + + validateTemplate(uriInfo, unsupportedParams); + + throwExceptionIfReqd(unsupportedParams); + + } + + private void throwExceptionIfReqd(List unsupportedParams) { + if (unsupportedParams.size() > 0) { + throw new UnsupportedParameterException(unsupportedParams); + } + } + + private void validateTemplate(final UriInfo uriInfo, + List unsupportedParams) { + final boolean templateRequest = ApiParameterHelper.template(uriInfo + .getQueryParameters()); + if (templateRequest) { + unsupportedParams.add("template"); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/service/AppuserLoansMapperReadService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/service/AppuserLoansMapperReadService.java new file mode 100644 index 000000000..387589c48 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/service/AppuserLoansMapperReadService.java @@ -0,0 +1,11 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.loanaccount.service; + +public interface AppuserLoansMapperReadService { + + public Boolean isLoanMappedToUser(Long loanId, Long appUserId); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/service/AppuserLoansMapperReadServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/service/AppuserLoansMapperReadServiceImpl.java new file mode 100644 index 000000000..9f18588d8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/loanaccount/service/AppuserLoansMapperReadServiceImpl.java @@ -0,0 +1,35 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.loanaccount.service; + +import org.mifosplatform.infrastructure.core.service.RoutingDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +@Service +public class AppuserLoansMapperReadServiceImpl implements + AppuserLoansMapperReadService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public AppuserLoansMapperReadServiceImpl(final RoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public Boolean isLoanMappedToUser(Long loanId, Long appUserId) { + return this.jdbcTemplate + .queryForObject( + "select case when (count(*) > 0) then true else false end " + + " from m_selfservice_user_client_mapping as m " + + " left join m_loan as l on l.client_id = m.client_id " + + " where l.id = ? and m.appuser_id = ? ", + new Object[] { loanId, appUserId }, Boolean.class); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/api/SelfSavingsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/api/SelfSavingsApiResource.java new file mode 100644 index 000000000..c290fc336 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/api/SelfSavingsApiResource.java @@ -0,0 +1,135 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.savings.api; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.savings.api.SavingsAccountChargesApiResource; +import org.mifosplatform.portfolio.savings.api.SavingsAccountTransactionsApiResource; +import org.mifosplatform.portfolio.savings.api.SavingsAccountsApiResource; +import org.mifosplatform.portfolio.savings.exception.SavingsAccountNotFoundException; +import org.mifosplatform.portfolio.self.savings.data.SelfSavingsDataValidator; +import org.mifosplatform.portfolio.self.savings.service.AppuserSavingsMapperReadService; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/self/savingsaccounts") +@Component +@Scope("singleton") +public class SelfSavingsApiResource { + + private final PlatformSecurityContext context; + private final SavingsAccountsApiResource savingsAccountsApiResource; + private final SavingsAccountChargesApiResource savingsAccountChargesApiResource; + private final SavingsAccountTransactionsApiResource savingsAccountTransactionsApiResource; + private final AppuserSavingsMapperReadService appuserSavingsMapperReadService; + private final SelfSavingsDataValidator dataValidator; + + @Autowired + public SelfSavingsApiResource( + final PlatformSecurityContext context, + final SavingsAccountsApiResource savingsAccountsApiResource, + final SavingsAccountChargesApiResource savingsAccountChargesApiResource, + final SavingsAccountTransactionsApiResource savingsAccountTransactionsApiResource, + final AppuserSavingsMapperReadService appuserSavingsMapperReadService, + final SelfSavingsDataValidator dataValidator) { + this.context = context; + this.savingsAccountsApiResource = savingsAccountsApiResource; + this.savingsAccountChargesApiResource = savingsAccountChargesApiResource; + this.savingsAccountTransactionsApiResource = savingsAccountTransactionsApiResource; + this.appuserSavingsMapperReadService = appuserSavingsMapperReadService; + this.dataValidator = dataValidator; + } + + @GET + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveSavings( + @PathParam("accountId") final Long accountId, + @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, + @Context final UriInfo uriInfo) { + + this.dataValidator.validateRetrieveSavings(uriInfo); + + validateAppuserSavingsAccountMapping(accountId); + + final boolean staffInSelectedOfficeOnly = false; + return this.savingsAccountsApiResource.retrieveOne(accountId, + staffInSelectedOfficeOnly, chargeStatus, uriInfo); + } + + @GET + @Path("{accountId}/transactions/{transactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveSavingsTransaction( + @PathParam("savingsId") final Long accountId, + @PathParam("transactionId") final Long transactionId, + @Context final UriInfo uriInfo) { + + this.dataValidator.validateRetrieveSavingsTransaction(uriInfo); + + validateAppuserSavingsAccountMapping(accountId); + + return this.savingsAccountTransactionsApiResource.retrieveOne( + accountId, transactionId, uriInfo); + } + + @GET + @Path("{accountId}/charges") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAllSavingsAccountCharges( + @PathParam("savingsAccountId") final Long accountId, + @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus, + @Context final UriInfo uriInfo) { + + validateAppuserSavingsAccountMapping(accountId); + + return this.savingsAccountChargesApiResource + .retrieveAllSavingsAccountCharges(accountId, chargeStatus, + uriInfo); + } + + @GET + @Path("{accountId}/charges/{savingsAccountChargeId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveSavingsAccountCharge( + @PathParam("savingsAccountId") final Long accountId, + @PathParam("savingsAccountChargeId") final Long savingsAccountChargeId, + @Context final UriInfo uriInfo) { + + validateAppuserSavingsAccountMapping(accountId); + + return this.savingsAccountChargesApiResource + .retrieveSavingsAccountCharge(accountId, + savingsAccountChargeId, uriInfo); + } + + private void validateAppuserSavingsAccountMapping(final Long accountId) { + AppUser user = this.context.authenticatedUser(); + final boolean isMappedSavings = this.appuserSavingsMapperReadService + .isSavingsMappedToUser(accountId, user.getId()); + if (!isMappedSavings) { + throw new SavingsAccountNotFoundException(accountId); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/data/SelfSavingsDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/data/SelfSavingsDataValidator.java new file mode 100644 index 000000000..a7544ab24 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/data/SelfSavingsDataValidator.java @@ -0,0 +1,73 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.savings.data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; +import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException; +import org.mifosplatform.portfolio.savings.SavingsApiConstants; +import org.springframework.stereotype.Component; + +@Component +public class SelfSavingsDataValidator { + + private static final Set allowedAssociationParameters = new HashSet<>( + Arrays.asList(SavingsApiConstants.transactions, + SavingsApiConstants.charges)); + + public void validateRetrieveSavings(final UriInfo uriInfo) { + List unsupportedParams = new ArrayList<>(); + + validateTemplate(uriInfo, unsupportedParams); + + Set associationParameters = ApiParameterHelper + .extractAssociationsForResponseIfProvided(uriInfo + .getQueryParameters()); + if (!associationParameters.isEmpty()) { + associationParameters.removeAll(allowedAssociationParameters); + if (!associationParameters.isEmpty()) { + unsupportedParams.addAll(associationParameters); + } + } + + if (uriInfo.getQueryParameters().getFirst("exclude") != null) { + unsupportedParams.add("exclude"); + } + + throwExceptionIfReqd(unsupportedParams); + } + + public void validateRetrieveSavingsTransaction(final UriInfo uriInfo) { + List unsupportedParams = new ArrayList<>(); + + validateTemplate(uriInfo, unsupportedParams); + + throwExceptionIfReqd(unsupportedParams); + } + + private void throwExceptionIfReqd(final List unsupportedParams) { + if (unsupportedParams.size() > 0) { + throw new UnsupportedParameterException(unsupportedParams); + } + } + + private void validateTemplate(final UriInfo uriInfo, + List unsupportedParams) { + final boolean templateRequest = ApiParameterHelper.template(uriInfo + .getQueryParameters()); + if (templateRequest) { + unsupportedParams.add("template"); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/service/AppuserSavingsMapperReadService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/service/AppuserSavingsMapperReadService.java new file mode 100644 index 000000000..da28da6ab --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/service/AppuserSavingsMapperReadService.java @@ -0,0 +1,12 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.savings.service; + +public interface AppuserSavingsMapperReadService { + + public Boolean isSavingsMappedToUser(Long savingsId, Long appUserId); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/service/AppuserSavingsMapperReadServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/service/AppuserSavingsMapperReadServiceImpl.java new file mode 100644 index 000000000..8261dd045 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/savings/service/AppuserSavingsMapperReadServiceImpl.java @@ -0,0 +1,35 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.savings.service; + +import org.mifosplatform.infrastructure.core.service.RoutingDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +@Service +public class AppuserSavingsMapperReadServiceImpl implements + AppuserSavingsMapperReadService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public AppuserSavingsMapperReadServiceImpl( + final RoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public Boolean isSavingsMappedToUser(Long savingsId, Long appUserId) { + return this.jdbcTemplate + .queryForObject( + "select case when (count(*) > 0) then true else false end " + + " from m_selfservice_user_client_mapping as m " + + " left join m_savings_account as s on s.client_id = m.client_id " + + " where s.id = ? and m.appuser_id = ? ", + new Object[] { savingsId, appUserId }, Boolean.class); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/security/api/SelfAuthenticationApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/security/api/SelfAuthenticationApiResource.java new file mode 100644 index 000000000..7a1348523 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/security/api/SelfAuthenticationApiResource.java @@ -0,0 +1,41 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.security.api; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.mifosplatform.infrastructure.security.api.AuthenticationApiResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/self/authentication") +@Component +@Profile("basicauth") +@Scope("singleton") +public class SelfAuthenticationApiResource { + + private final AuthenticationApiResource authenticationApiResource; + + @Autowired + public SelfAuthenticationApiResource( + final AuthenticationApiResource authenticationApiResource) { + this.authenticationApiResource = authenticationApiResource; + } + + @POST + @Produces({ MediaType.APPLICATION_JSON }) + public String authenticate(@QueryParam("username") final String username, + @QueryParam("password") final String password) { + return this.authenticationApiResource.authenticate(username, password); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/security/api/SelfUserDetailsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/security/api/SelfUserDetailsApiResource.java new file mode 100644 index 000000000..d0855a5ea --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/self/security/api/SelfUserDetailsApiResource.java @@ -0,0 +1,41 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.self.security.api; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.mifosplatform.infrastructure.security.api.UserDetailsApiResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/self/userdetails") +@Component +@Profile("oauth") +@Scope("singleton") +public class SelfUserDetailsApiResource { + + private final UserDetailsApiResource userDetailsApiResource; + + @Autowired + public SelfUserDetailsApiResource( + final UserDetailsApiResource userDetailsApiResource) { + this.userDetailsApiResource = userDetailsApiResource; + } + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + public String fetchAuthenticatedUserData( + @QueryParam("access_token") final String accessToken) { + return this.userDetailsApiResource + .fetchAuthenticatedUserData(accessToken); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/data/AppUserData.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/data/AppUserData.java index 5c7f711d9..d2a1a00d8 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/data/AppUserData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/data/AppUserData.java @@ -6,9 +6,11 @@ package org.mifosplatform.useradministration.data; import java.util.Collection; +import java.util.Set; import org.mifosplatform.organisation.office.data.OfficeData; import org.mifosplatform.organisation.staff.data.StaffData; +import org.mifosplatform.portfolio.client.data.ClientData; /** * Immutable data object for application user data. @@ -29,31 +31,35 @@ public class AppUserData { private final Collection availableRoles; private final Collection selectedRoles; private final StaffData staff; + private final Boolean isSelfServiceUser; + + @SuppressWarnings("unused") + private Set clients; public static AppUserData template(final AppUserData user, final Collection officesForDropdown) { return new AppUserData(user.id, user.username, user.email, user.officeId, user.officeName, user.firstname, user.lastname, - user.availableRoles, user.selectedRoles, officesForDropdown, user.staff, user.passwordNeverExpires); + user.availableRoles, user.selectedRoles, officesForDropdown, user.staff, user.passwordNeverExpires, user.isSelfServiceUser); } public static AppUserData template(final Collection offices, final Collection availableRoles) { - return new AppUserData(null, null, null, null, null, null, null, availableRoles, null, offices, null, null); + return new AppUserData(null, null, null, null, null, null, null, availableRoles, null, offices, null, null, null); } public static AppUserData dropdown(final Long id, final String username) { - return new AppUserData(id, username, null, null, null, null, null, null, null, null, null, null); + return new AppUserData(id, username, null, null, null, null, null, null, null, null, null, null, null); } public static AppUserData instance(final Long id, final String username, final String email, final Long officeId, final String officeName, final String firstname, final String lastname, final Collection availableRoles, - final Collection selectedRoles, final StaffData staff, final Boolean passwordNeverExpire) { + final Collection selectedRoles, final StaffData staff, final Boolean passwordNeverExpire, final Boolean isSelfServiceUser) { return new AppUserData(id, username, email, officeId, officeName, firstname, lastname, availableRoles, selectedRoles, null, staff, - passwordNeverExpire); + passwordNeverExpire, isSelfServiceUser); } private AppUserData(final Long id, final String username, final String email, final Long officeId, final String officeName, final String firstname, final String lastname, final Collection availableRoles, final Collection selectedRoles, final Collection allowedOffices, final StaffData staff, - final Boolean passwordNeverExpire) { + final Boolean passwordNeverExpire, final Boolean isSelfServiceUser) { this.id = id; this.username = username; this.officeId = officeId; @@ -66,6 +72,7 @@ public class AppUserData { this.selectedRoles = selectedRoles; this.staff = staff; this.passwordNeverExpires = passwordNeverExpire; + this.isSelfServiceUser = isSelfServiceUser; } public boolean hasIdentifyOf(final Long createdById) { @@ -92,4 +99,13 @@ public class AppUserData { public int hashCode() { return id != null ? id.hashCode() : 0; } + + public void setClients(Set clients){ + this.clients = clients; + } + + public boolean isSelfServiceUser() { + return this.isSelfServiceUser==null?false:this.isSelfServiceUser; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java index c060c00fe..8cd02a91b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -21,11 +22,14 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.service.DateUtils; @@ -35,6 +39,7 @@ import org.mifosplatform.infrastructure.security.service.PlatformPasswordEncoder import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.staff.domain.Staff; +import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.useradministration.service.AppUserConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,7 +106,16 @@ public class AppUser extends AbstractPersistable implements PlatformUser { @Column(name = "password_never_expires", nullable = false) private boolean passwordNeverExpires; - public static AppUser fromJson(final Office userOffice, final Staff linkedStaff, final Set allRoles, final JsonCommand command) { + @Column(name = "is_self_service_user", nullable = false) + private boolean isSelfServiceUser; + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "appuser_id", referencedColumnName= "id", nullable = false) + private Set appUserClientMappings = new HashSet<>(); + + public static AppUser fromJson(final Office userOffice, final Staff linkedStaff, final Set allRoles, + final Collection clients, final JsonCommand command) { final String username = command.stringValueOfParameterNamed("username"); String password = command.stringValueOfParameterNamed("password"); @@ -131,8 +145,11 @@ public class AppUser extends AbstractPersistable implements PlatformUser { final String email = command.stringValueOfParameterNamed("email"); final String firstname = command.stringValueOfParameterNamed("firstname"); final String lastname = command.stringValueOfParameterNamed("lastname"); + + final boolean isSelfServiceUser = command.booleanPrimitiveValueOfParameterNamed(AppUserConstants.IS_SELF_SERVICE_USER); - return new AppUser(userOffice, user, allRoles, email, firstname, lastname, linkedStaff, passwordNeverExpire); + return new AppUser(userOffice, user, allRoles, email, firstname, lastname, linkedStaff, passwordNeverExpire, + isSelfServiceUser, clients); } protected AppUser() { @@ -142,7 +159,8 @@ public class AppUser extends AbstractPersistable implements PlatformUser { } public AppUser(final Office office, final User user, final Set roles, final String email, final String firstname, - final String lastname, final Staff staff, final boolean passwordNeverExpire) { + final String lastname, final Staff staff, final boolean passwordNeverExpire, + final boolean isSelfServiceUser, final Collection clients) { this.office = office; this.email = email.trim(); this.username = user.getUsername().trim(); @@ -158,6 +176,8 @@ public class AppUser extends AbstractPersistable implements PlatformUser { this.lastTimePasswordUpdated = DateUtils.getDateOfTenant(); this.staff = staff; this.passwordNeverExpires = passwordNeverExpire; + this.isSelfServiceUser = isSelfServiceUser; + this.appUserClientMappings = createAppUserClientMappings(clients); } public EnumOptionData organisationalRoleData() { @@ -190,7 +210,8 @@ public class AppUser extends AbstractPersistable implements PlatformUser { } } - public Map update(final JsonCommand command, final PlatformPasswordEncoder platformPasswordEncoder) { + public Map update(final JsonCommand command, final PlatformPasswordEncoder platformPasswordEncoder, + final Collection clients) { final Map actualChanges = new LinkedHashMap<>(7); @@ -270,6 +291,30 @@ public class AppUser extends AbstractPersistable implements PlatformUser { this.passwordNeverExpires = newValue; } } + + if(command.hasParameter(AppUserConstants.IS_SELF_SERVICE_USER)){ + if (command.isChangeInBooleanParameterNamed(AppUserConstants.IS_SELF_SERVICE_USER, this.isSelfServiceUser)){ + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(AppUserConstants.IS_SELF_SERVICE_USER); + actualChanges.put(AppUserConstants.IS_SELF_SERVICE_USER, newValue); + this.isSelfServiceUser = newValue; + } + } + + if(this.isSelfServiceUser && command.hasParameter(AppUserConstants.CLIENTS)){ + actualChanges.put(AppUserConstants.CLIENTS, command.arrayValueOfParameterNamed(AppUserConstants.CLIENTS)); + Set newClients = createAppUserClientMappings(clients); + if(this.appUserClientMappings == null){ + this.appUserClientMappings = new HashSet<>(); + }else{ + this.appUserClientMappings.retainAll(newClients); + } + this.appUserClientMappings.addAll(newClients); + }else if(!this.isSelfServiceUser && actualChanges.containsKey(AppUserConstants.IS_SELF_SERVICE_USER)){ + actualChanges.put(AppUserConstants.CLIENTS, new ArrayList<>()); + if(this.appUserClientMappings != null){ + this.appUserClientMappings = null; + } + } return actualChanges; } @@ -573,4 +618,24 @@ public class AppUser extends AbstractPersistable implements PlatformUser { public boolean isNotEnabled() { return !isEnabled(); } + + public boolean isSelfServiceUser() { + return this.isSelfServiceUser; + } + + public Set getAppUserClientMappings() { + return this.appUserClientMappings; + } + + private Set createAppUserClientMappings(Collection clients) { + Set newAppUserClientMappings = null; + if(clients != null && clients.size() > 0){ + newAppUserClientMappings = new HashSet<>(); + for(Client client : clients){ + newAppUserClientMappings.add(new AppUserClientMapping(client)); + } + } + return newAppUserClientMappings; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUserClientMapping.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUserClientMapping.java new file mode 100644 index 000000000..11e3c9884 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUserClientMapping.java @@ -0,0 +1,67 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.useradministration.domain; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.mifosplatform.portfolio.client.domain.Client; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_selfservice_user_client_mapping") +public class AppUserClientMapping extends AbstractPersistable { + + @ManyToOne(optional = false, cascade = CascadeType.PERSIST) + @JoinColumn(name = "client_id", nullable = false) + private Client client; + + public AppUserClientMapping(){ + + } + + public AppUserClientMapping(Client client){ + this.client = client; + } + + public Client getClient() { + return this.client; + } + + @Override + public boolean equals(Object obj) { + + if (null == obj) { + return false; + } + + if (this == obj) { + return true; + } + + if (!getClass().equals(obj.getClass())) { + return false; + } + + AppUserClientMapping that = (AppUserClientMapping) obj; + + return null == this.client.getId() ? false : this.client.getId().equals(that.client.getId()); + } + + @Override + public int hashCode() { + + int hashCode = 17; + + hashCode += null == this.client ? 0 : this.client.getId().hashCode() * 31; + + return hashCode; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserConstants.java index 1fb25bf91..4d017a429 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserConstants.java @@ -8,5 +8,7 @@ package org.mifosplatform.useradministration.service; public class AppUserConstants { public static final String PASSWORD_NEVER_EXPIRES = "passwordNeverExpires"; + public static final String IS_SELF_SERVICE_USER = "isSelfServiceUser"; + public static final String CLIENTS = "clients"; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserReadPlatformServiceImpl.java index c234832a1..976f5bc5a 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserReadPlatformServiceImpl.java @@ -9,6 +9,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.Set; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; @@ -18,9 +19,12 @@ import org.mifosplatform.organisation.office.data.OfficeData; import org.mifosplatform.organisation.office.service.OfficeReadPlatformService; import org.mifosplatform.organisation.staff.data.StaffData; import org.mifosplatform.organisation.staff.service.StaffReadPlatformService; +import org.mifosplatform.portfolio.client.data.ClientData; +import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.useradministration.data.AppUserData; import org.mifosplatform.useradministration.data.RoleData; import org.mifosplatform.useradministration.domain.AppUser; +import org.mifosplatform.useradministration.domain.AppUserClientMapping; import org.mifosplatform.useradministration.domain.AppUserRepository; import org.mifosplatform.useradministration.domain.Role; import org.mifosplatform.useradministration.exception.UserNotFoundException; @@ -119,9 +123,21 @@ public class AppUserReadPlatformServiceImpl implements AppUserReadPlatformServic linkedStaff = null; } - return AppUserData.instance(user.getId(), user.getUsername(), user.getEmail(), user.getOffice().getId(), + AppUserData retUser = AppUserData.instance(user.getId(), user.getUsername(), user.getEmail(), user.getOffice().getId(), user.getOffice().getName(), user.getFirstname(), user.getLastname(), availableRoles, selectedUserRoles, linkedStaff, - user.getPasswordNeverExpires()); + user.getPasswordNeverExpires(), user.isSelfServiceUser()); + + if(retUser.isSelfServiceUser()){ + Set clients = new HashSet<>(); + for(AppUserClientMapping clientMap : user.getAppUserClientMappings()){ + Client client = clientMap.getClient(); + clients.add(ClientData.lookup(client.getId(), client.getDisplayName(), + client.getOffice().getId(), client.getOffice().getName())); + } + retUser.setClients(clients); + } + + return retUser; } private static final class AppUserMapper implements RowMapper { @@ -146,6 +162,7 @@ public class AppUserReadPlatformServiceImpl implements AppUserReadPlatformServic final String officeName = rs.getString("officeName"); final Long staffId = JdbcSupport.getLong(rs, "staffId"); final Boolean passwordNeverExpire = rs.getBoolean("passwordNeverExpires"); + final Boolean isSelfServiceUser = rs.getBoolean("isSelfServiceUser"); final Collection selectedRoles = this.roleReadPlatformService.retrieveAppUserRoles(id); final StaffData linkedStaff; @@ -155,12 +172,12 @@ public class AppUserReadPlatformServiceImpl implements AppUserReadPlatformServic linkedStaff = null; } return AppUserData.instance(id, username, email, officeId, officeName, firstname, lastname, null, selectedRoles, linkedStaff, - passwordNeverExpire); + passwordNeverExpire, isSelfServiceUser); } public String schema() { return " u.id as id, u.username as username, u.firstname as firstname, u.lastname as lastname, u.email as email, u.password_never_expires as passwordNeverExpires, " - + " u.office_id as officeId, o.name as officeName, u.staff_id as staffId from m_appuser u " + + " u.office_id as officeId, o.name as officeName, u.staff_id as staffId, u.is_self_service_user as isSelfServiceUser from m_appuser u " + " join m_office o on o.id = u.office_id where o.hierarchy like ? and u.is_deleted=0 order by u.username"; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserWritePlatformServiceJpaRepositoryImpl.java index 4b7692f52..3b75dd89a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/AppUserWritePlatformServiceJpaRepositoryImpl.java @@ -6,6 +6,7 @@ package org.mifosplatform.useradministration.service; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -26,6 +27,8 @@ import org.mifosplatform.organisation.office.domain.OfficeRepository; import org.mifosplatform.organisation.office.exception.OfficeNotFoundException; import org.mifosplatform.organisation.staff.domain.Staff; import org.mifosplatform.organisation.staff.domain.StaffRepositoryWrapper; +import org.mifosplatform.portfolio.client.domain.Client; +import org.mifosplatform.portfolio.client.domain.ClientRepository; import org.mifosplatform.useradministration.api.AppUserApiConstant; import org.mifosplatform.useradministration.domain.AppUser; import org.mifosplatform.useradministration.domain.AppUserPreviousPassword; @@ -49,6 +52,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + @Service public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWritePlatformService { @@ -63,12 +69,14 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit private final UserDataValidator fromApiJsonDeserializer; private final AppUserPreviousPasswordRepository appUserPreviewPasswordRepository; private final StaffRepositoryWrapper staffRepositoryWrapper; + private final ClientRepository clientRepository; @Autowired public AppUserWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final AppUserRepository appUserRepository, final UserDomainService userDomainService, final OfficeRepository officeRepository, final RoleRepository roleRepository, final PlatformPasswordEncoder platformPasswordEncoder, final UserDataValidator fromApiJsonDeserializer, - final AppUserPreviousPasswordRepository appUserPreviewPasswordRepository, final StaffRepositoryWrapper staffRepositoryWrapper) { + final AppUserPreviousPasswordRepository appUserPreviewPasswordRepository, final StaffRepositoryWrapper staffRepositoryWrapper, + final ClientRepository clientRepository) { this.context = context; this.appUserRepository = appUserRepository; this.userDomainService = userDomainService; @@ -78,6 +86,7 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.appUserPreviewPasswordRepository = appUserPreviewPasswordRepository; this.staffRepositoryWrapper = staffRepositoryWrapper; + this.clientRepository = clientRepository; } @Transactional @@ -108,8 +117,20 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit if (staffId != null) { linkedStaff = this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userOffice.getId()); } + + Collection clients = null; + if(command.hasParameter(AppUserConstants.IS_SELF_SERVICE_USER) + && command.booleanPrimitiveValueOfParameterNamed(AppUserConstants.IS_SELF_SERVICE_USER) + && command.hasParameter(AppUserConstants.CLIENTS)){ + JsonArray clientsArray = command.arrayOfParameterNamed(AppUserConstants.CLIENTS); + Collection clientIds = new HashSet<>(); + for(JsonElement clientElement : clientsArray){ + clientIds.add(clientElement.getAsLong()); + } + clients = this.clientRepository.findAll(clientIds); + } - appUser = AppUser.fromJson(userOffice, linkedStaff, allRoles, command); + appUser = AppUser.fromJson(userOffice, linkedStaff, allRoles, clients, command); final Boolean sendPasswordToEmail = command.booleanObjectValueOfParameterNamed("sendPasswordToEmail"); this.userDomainService.create(appUser, sendPasswordToEmail); @@ -151,8 +172,24 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit if (userToUpdate == null) { throw new UserNotFoundException(userId); } final AppUserPreviousPassword currentPasswordToSaveAsPreview = getCurrentPasswordToSaveAsPreview(userToUpdate, command); + + Collection clients = null; + boolean isSelfServiceUser = userToUpdate.isSelfServiceUser(); + if(command.hasParameter(AppUserConstants.IS_SELF_SERVICE_USER)){ + isSelfServiceUser = command.booleanPrimitiveValueOfParameterNamed(AppUserConstants.IS_SELF_SERVICE_USER); + } + + if(isSelfServiceUser + && command.hasParameter(AppUserConstants.CLIENTS)){ + JsonArray clientsArray = command.arrayOfParameterNamed(AppUserConstants.CLIENTS); + Collection clientIds = new HashSet<>(); + for(JsonElement clientElement : clientsArray){ + clientIds.add(clientElement.getAsLong()); + } + clients = this.clientRepository.findAll(clientIds); + } - final Map changes = userToUpdate.update(command, this.platformPasswordEncoder); + final Map changes = userToUpdate.update(command, this.platformPasswordEncoder, clients); if (changes.containsKey("officeId")) { final Long officeId = (Long) changes.get("officeId"); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/UserDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/UserDataValidator.java index bd3b633db..ce8a16f44 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/UserDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/service/UserDataValidator.java @@ -24,6 +24,7 @@ import org.mifosplatform.useradministration.domain.PasswordValidationPolicyRepos import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; @@ -34,7 +35,8 @@ public final class UserDataValidator { * The parameters supported for this command. */ private final Set supportedParameters = new HashSet<>(Arrays.asList("username", "firstname", "lastname", "password", - "repeatPassword", "email", "officeId", "notSelectedRoles", "roles", "sendPasswordToEmail", "staffId", "passwordNeverExpires")); + "repeatPassword", "email", "officeId", "notSelectedRoles", "roles", "sendPasswordToEmail", "staffId", "passwordNeverExpires", + AppUserConstants.IS_SELF_SERVICE_USER, AppUserConstants.CLIENTS)); private final FromJsonHelper fromApiJsonHelper; @@ -100,6 +102,29 @@ public final class UserDataValidator { .extractBooleanNamed(AppUserConstants.PASSWORD_NEVER_EXPIRES, element); baseDataValidator.reset().parameter("passwordNeverExpire").value(passwordNeverExpire).validateForBooleanValue(); } + + Boolean isSelfServiceUser = null; + if(this.fromApiJsonHelper.parameterExists(AppUserConstants.IS_SELF_SERVICE_USER, element)){ + isSelfServiceUser = this.fromApiJsonHelper.extractBooleanNamed(AppUserConstants.IS_SELF_SERVICE_USER, element); + if(isSelfServiceUser == null){ + baseDataValidator.reset().parameter(AppUserConstants.IS_SELF_SERVICE_USER).trueOrFalseRequired(false); + } + } + + if(this.fromApiJsonHelper.parameterExists(AppUserConstants.CLIENTS, element)){ + if(isSelfServiceUser == null || !isSelfServiceUser){ + baseDataValidator.reset().parameter(AppUserConstants.CLIENTS).failWithCode("not.supported.when.isSelfServiceUser.is.false", + "clients parameter is not supported when isSelfServiceUser parameter is false"); + }else{ + final JsonArray clientsArray = this.fromApiJsonHelper.extractJsonArrayNamed(AppUserConstants.CLIENTS, element); + baseDataValidator.reset().parameter(AppUserConstants.CLIENTS).value(clientsArray).jsonArrayNotEmpty(); + + for(JsonElement client : clientsArray){ + Long clientId = client.getAsLong(); + baseDataValidator.reset().parameter(AppUserConstants.CLIENTS).value(clientId).longGreaterThanZero(); + } + } + } final String[] roles = this.fromApiJsonHelper.extractArrayNamed("roles", element); baseDataValidator.reset().parameter("roles").value(roles).arrayNotEmpty(); @@ -175,6 +200,29 @@ public final class UserDataValidator { final boolean passwordNeverExpire = this.fromApiJsonHelper.extractBooleanNamed("passwordNeverExpire", element); baseDataValidator.reset().parameter("passwordNeverExpire").value(passwordNeverExpire).validateForBooleanValue(); } + + Boolean isSelfServiceUser = null; + if(this.fromApiJsonHelper.parameterExists(AppUserConstants.IS_SELF_SERVICE_USER, element)){ + isSelfServiceUser = this.fromApiJsonHelper.extractBooleanNamed(AppUserConstants.IS_SELF_SERVICE_USER, element); + if(isSelfServiceUser == null){ + baseDataValidator.reset().parameter(AppUserConstants.IS_SELF_SERVICE_USER).trueOrFalseRequired(false); + } + } + + if(this.fromApiJsonHelper.parameterExists(AppUserConstants.CLIENTS, element)){ + if(isSelfServiceUser != null && !isSelfServiceUser){ + baseDataValidator.reset().parameter(AppUserConstants.CLIENTS).failWithCode("not.supported.when.isSelfServiceUser.is.false", + "clients parameter is not supported when isSelfServiceUser parameter is false"); + }else{ + final JsonArray clientsArray = this.fromApiJsonHelper.extractJsonArrayNamed(AppUserConstants.CLIENTS, element); + baseDataValidator.reset().parameter(AppUserConstants.CLIENTS).value(clientsArray).jsonArrayNotEmpty(); + + for(JsonElement client : clientsArray){ + Long clientId = client.getAsLong(); + baseDataValidator.reset().parameter(AppUserConstants.CLIENTS).value(clientId).longGreaterThanZero(); + } + } + } throwExceptionIfValidationWarningsExist(dataValidationErrors); } diff --git a/mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml b/mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml index abb47a67e..e2bb9a1bb 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/securityContext.xml @@ -25,6 +25,8 @@ entry-point-ref="basicAuthenticationEntryPoint"> + + diff --git a/mifosng-provider/src/main/resources/sql/migrations/core_db/V282__CustomerSelfService.sql b/mifosng-provider/src/main/resources/sql/migrations/core_db/V282__CustomerSelfService.sql new file mode 100644 index 000000000..5b60c0b49 --- /dev/null +++ b/mifosng-provider/src/main/resources/sql/migrations/core_db/V282__CustomerSelfService.sql @@ -0,0 +1,15 @@ +ALTER TABLE `m_appuser` +ADD COLUMN `is_self_service_user` BIT(1) NOT NULL DEFAULT 0; + +CREATE TABLE `m_selfservice_user_client_mapping` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `appuser_id` BIGINT(20) NOT NULL, + `client_id` BIGINT(20) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `appuser_id_client_id` (`appuser_id`, `client_id`), + CONSTRAINT `m_selfservice_appuser_id` FOREIGN KEY (`appuser_id`) REFERENCES `m_appuser` (`id`), + CONSTRAINT `m_selfservice_client_id` FOREIGN KEY (`client_id`) REFERENCES `m_client` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB +AUTO_INCREMENT=1; \ No newline at end of file