Персональные цены для договоров

Материал из BiTel WiKi

Перейти к: навигация, поиск

Внимание! Данное решение/метод/статья относится к версии 5.2 и для других версий может быть неактуальна! Вам нужно самостоятельно поправить решение под свои нужды или воспользоваться помощью на форуме. Будем признательны, если внизу страницы или отдельной статьёй вы разместите исправленное решение для другой версии или подсказки что надо исправить.

Содержание

Введение

Очень часто, особенно при работе с юр лицами, возникает потребность установки персональной цены абонплат для стандартных тарифов. В текущей реализации биллинга это можно сделать только через персональные тарифы. Но персональные тарифы очень неудобны:

  • Ломают отчёты по тарифам
  • Осложняют внесение изменений в глобальные тарифы, от которых они наследованы
  • Требуют специальных знаний и прав доступа для корректного использования

В результате было решено разработать собственное решение для поддержки персональных цен без применения персональных тарифов

Описание решения

Тезисно решение выглядит следующим образом

  1. Храним персональные цены на услуги в собственной таблице custom_tariff_cost: contract_tariff_id, sid -> value
  2. Меняем стандартную логику тарифного узла "стоимость" модуля абонплат: при наличии записи в custom_tariff_cost используем её вместо стандартной цены
  3. Пишем собственную серверную и клиентскую части интерфейса для управления переопределёнными ценами
  4. Дорабатываем выдачу списка тарифов договора для визуального отображения переопределённых цен

!!! Внимание! пп. 2 и 3 являются "хаками" стандартного поведения биллинга: требуется подмена стандартных классов своими !!!

Скриншоты:

Интерфейс переопределения цен:

Файл:custom_cost.jpg

Отображение на договоре:

Файл:custom_cost1.jpg

В тарифном дереве:

Файл:custom_cost2.jpg

Реализация

Переопределение цены

Таблица для хранения переопределений цен

CREATE TABLE `custom_tariff_cost` (
  `sid` int(10) UNSIGNED NOT NULL,
  `value` decimal(15,5) NOT NULL,
  `contract_tariff_id` int(10) UNSIGNED NOT NULL,
  PRIMARY KEY (`sid`,`contract_tariff_id`),
  KEY `sid` (`sid`)

Обратите внимания, что переопределяется цена на услугу не для договора (и всех его тарифов), а для _тарифа_ договора (custom_tariff_cost.contract_tariff_id->contract_tariff.id).

Это сделано осознанно, по опыту проблем с тарифными опциями, которые на деле являются опциями договора, хотя и ориентированы на конкретные тарифы.

Другой момент - в таблице нет дат. Это тоже сделано осознанно, чтобы не плодить сущности. Т.о. если требуется изменить на том же тарифе для договора персональную цену, то нужно закрыть датой тариф договора с предыдущей ценой и открыть датой тот же глобальный тариф с новой ценой.

Кэш таблицы переопределений цен в памяти для ускорения тарификации

Для начала напишем DAO для работы с таблицей.

Объект:

package ru.dsi.bgbilling.kernel.discount.api.common.bean;
 
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
 
/**
 * @author cromeshnic@gmail.com
 */
public class CustomTariffCost implements Serializable {
    private int contract_tariff_id;
    private int sid;
    private BigDecimal value;
    private Date date1;
    private Date date2;
 
    public CustomTariffCost(){
        this.contract_tariff_id = -1;
        this.sid = -1;
        this.value = null;
        this.date1 = null;
        this.date2 = null;
    }
 
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        sb.append("contract_tariff_id = ")
                .append(this.contract_tariff_id)
                .append(", sid = ")
                .append(this.sid)
                .append(", value = ")
                .append(this.value)
                .append("]");
        return sb.toString();
    }
 
    //public ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost(int cid, int sid, int contract_tariff_id, BigDecimal value, Date date1, Date date2) {
    public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value, Date date1, Date date2) {
        //this.cid = cid;
        this.contract_tariff_id = contract_tariff_id;
        this.sid = sid;
        this.value = value;
        this.date1 = date1;
        this.date2 = date2;
    }
 
    public CustomTariffCost(int contract_tariff_id, int sid, BigDecimal value){
        this.contract_tariff_id = contract_tariff_id;
        this.sid = sid;
        this.value = value;
        this.date1 = null;
        this.date2 = null;
    }
 
    public int getContract_tariff_id() {
        return contract_tariff_id;
    }
 
    public void setContract_tariff_id(int contract_tariff_id) {
        this.contract_tariff_id = contract_tariff_id;
    }
 
    public int getSid() {
        return sid;
    }
 
    public void setSid(int sid) {
        this.sid = sid;
    }
 
    public BigDecimal getValue() {
        return value;
    }
 
    public void setValue(BigDecimal value) {
        this.value = value;
    }
 
    public Date getDate1() {
        return date1;
    }
 
    public void setDate1(Date date1) {
        this.date1 = date1;
    }
 
    public Date getDate2() {
        return date2;
    }
 
    public void setDate2(Date date2) {
        this.date2 = date2;
    }
}

События изменения (для кэша):

package ru.dsi.bgbilling.kernel.discount.bean.event;
 
import ru.bitel.bgbilling.kernel.event.Event;
import ru.bitel.common.SerialUtils;
 
import javax.xml.bind.annotation.XmlRootElement;
 
/**
 * @author cromeshnic@gmail.com
 * Событие при изменении персональной цены на договоре
 */
@XmlRootElement
public class ContractCustomTariffCostsChangedEvent extends Event//QueueEvent
{
    private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);
 
    public ContractCustomTariffCostsChangedEvent(int cid, int userId) {
        super(0, cid, userId);
    }
 
    protected ContractCustomTariffCostsChangedEvent(){
    }
 
}
package ru.dsi.bgbilling.kernel.discount.bean.event;
 
import ru.bitel.bgbilling.kernel.event.Event;
import ru.bitel.common.SerialUtils;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
 
import javax.xml.bind.annotation.XmlRootElement;
 
/**
 * @author cromeshnic@gmail.com
 * Событие при изменении персональной цены на тарифе договора
 */
@XmlRootElement
public class CustomTariffCostChangedEvent extends Event//QueueEvent
{
    private static final long serialVersionUID = SerialUtils.generateSerialVersionUID(ContractCustomTariffCostsChangedEvent.class);
    private CustomTariffCost cost;
 
    public CustomTariffCostChangedEvent(CustomTariffCost cost, int cid, int userId) {
        super(0, cid, userId);
        this.cost = cost;
    }
 
    public CustomTariffCost getCost(){
        return this.cost;
    }
 
    protected CustomTariffCostChangedEvent(){
        this.cost = null;
    }
 
}

DAO:

package ru.dsi.bgbilling.kernel.discount.bean;
 
import bitel.billing.common.TimeUtils;
import bitel.billing.server.contract.bean.ContractTariff;
import bitel.billing.server.contract.bean.ContractTariffManager;
import org.apache.log4j.Logger;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.server.util.ServerUtils;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;
import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
 
/**
 * @author cromeshnic@gmail.com
 * Dao для работы с персональными ценами
 */
public class CustomTariffCostDao{
    protected Connection con;
 
    private static final Logger logger = Logger.getLogger(CustomTariffCostDao.class);
 
    public CustomTariffCostDao(Connection con) {
        this.con = con;
    }
 
    private void removeContractCustomTariffCosts(int contract_tariff_id) throws SQLException{
        PreparedStatement ps = this.con.prepareStatement("delete from custom_tariff_cost where contract_tariff_id=?");
        ps.setInt(1, contract_tariff_id);
        ps.executeUpdate();
        ps.close();
    }
 
    public void updateContractCustomTariffCosts(ContractTariff ct, List<CustomTariffCost> costs, int userId) throws SQLException, BGException{
        this.removeContractCustomTariffCosts(ct.getId());
        if(null!=costs){
            for(CustomTariffCost cost : costs){
                if(ct.getId()!=cost.getContract_tariff_id()){
                    throw new BGException("Attempt to update custom tariff cost: "+cost.toString()+" for another contract tariff = "+ct.getId());
                }
                this.update0(cost);
            }
        }
 
        EventProcessor.getInstance().publish(new
                ContractCustomTariffCostsChangedEvent(ct.getContractId(), userId));
    }
 
    public CustomTariffCost update(CustomTariffCost cost, int userId) throws SQLException, BGException {
        ContractTariffManager ctm = new ContractTariffManager(this.con);
        ContractTariff ct = ctm.getContractTariffById(cost.getContract_tariff_id());
        cost.setDate1(TimeUtils.convertCalendarToDate(ct.getDate1()));
        cost.setDate2(TimeUtils.convertCalendarToDate(ct.getDate2()));
        update0(cost);
 
        EventProcessor.getInstance().publish(new
                CustomTariffCostChangedEvent(cost, ct.getContractId(), userId));
        return cost;
    }
 
    private void update0(CustomTariffCost cost) throws SQLException, BGException{
        PreparedStatement ps = this.con.prepareStatement(
                "insert into custom_tariff_cost (contract_tariff_id, sid, value) values(?,?,?) " +
                        "on duplicate key update value=?");
        ps.setInt(1, cost.getContract_tariff_id());
        ps.setInt(2, cost.getSid());
        ps.setBigDecimal(3, cost.getValue());
        ps.setBigDecimal(4, cost.getValue());
        ps.executeUpdate();
        ps.close();
        ServerUtils.commitConnection(this.con);
    }
 
    public List<CustomTariffCost> getContractTariffCosts(ContractTariff ct) throws BGException {
        List<CustomTariffCost> result = new ArrayList<CustomTariffCost>();
        try{
            PreparedStatement ps = this.con.prepareStatement("select " +
                    "   contract_tariff_id, " +
                    "   sid, " +
                    "   value " +
                    "from " +
                    "   custom_tariff_cost ctc " +
                    "where " +
                    "   ctc.contract_tariff_id=? ");
            ps.setInt(1, ct.getContractId());
            ResultSet rs = ps.executeQuery();
            while(rs.next()){
                result.add(new CustomTariffCost(
                        rs.getInt(1),
                        rs.getInt(2),
                        rs.getBigDecimal(3),
                        TimeUtils.convertCalendarToDate(ct.getDate1()),
                        TimeUtils.convertCalendarToDate(ct.getDate2())));
            }
            rs.close();
            ps.close();
        }catch (SQLException e){
            processException(e);
        }
 
        return result;
    }
 
    public List<CustomTariffCost> getContractTariffCosts(int contract_tariff_id) throws BGException{
        List<CustomTariffCost> result = new ArrayList<CustomTariffCost>();
        try{
            PreparedStatement ps = this.con.prepareStatement("select " +
                    "   ctc.contract_tariff_id, " +
                    "   ctc.sid, " +
                    "   ctc.value," +
                    "   ct.date1," +
                    "   ct.date2 " +
                    "from " +
                    "   custom_tariff_cost ctc left join " +
                    "   contract_tariff ct on ctc.contract_tariff_id=ct.id " +
                    "where " +
                    "   ctc.contract_tariff_id=? ");
            ps.setInt(1, contract_tariff_id);
            ResultSet rs = ps.executeQuery();
            while(rs.next()){
                result.add(new CustomTariffCost(
                        rs.getInt(1),
                        rs.getInt(2),
                        rs.getBigDecimal(3),
                        TimeUtils.convertSqlDateToDate(rs.getDate(4)),
                        TimeUtils.convertSqlDateToDate(rs.getDate(5))));
            }
            rs.close();
            ps.close();
        }catch (SQLException e){
            processException(e);
        }
 
        return result;
    }
 
    protected void processException(SQLException e)
            throws BGException
    {
        throw new BGException(e.getMessage() + " [" + e.getSQLState() + ", " + e.getErrorCode() + "]", e);
    }
}

Сам кэш переопределений цен:

package ru.dsi.bgbilling.kernel.discount.bean;
 
import bitel.billing.common.TimeUtils;
import org.apache.log4j.Logger;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.event.EventListener;
import ru.bitel.bgbilling.kernel.event.EventListenerContext;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.kernel.tariff.server.event.ContractTariffChangedEvent;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.event.ContractCustomTariffCostsChangedEvent;
import ru.dsi.bgbilling.kernel.discount.bean.event.CustomTariffCostChangedEvent;
 
import java.lang.ref.SoftReference;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
 
/**
 * Кэш переопределённых цен на договоре
 */
public class CustomTariffCostCache {
 
    private static volatile CustomTariffCostCache instance = new CustomTariffCostCache();
    private static final Logger logger = Logger.getLogger(CustomTariffCostCache.class);
 
    //cid -> tree_id -> sid -> List<ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost>
    private final ConcurrentMap<Integer, SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>> tariffCostMap =
            new ConcurrentHashMap<Integer, SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>>();
 
    private CustomTariffCostCache() {
        new Thread("custom-tariff-cache-reload")
        {
            public void run()
            {
                try
                {
                    //При изменении переопределённых цен на тарифе обновляем все переопределения для cid
                    EventProcessor.getInstance().addListener(new EventListener<CustomTariffCostChangedEvent>()
                            {
                                @Override
                                public void notify(CustomTariffCostChangedEvent e, EventListenerContext ctx)
                                        throws BGException
                                {
                                    try {
                                        CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());
                                    } catch (SQLException ex) {
                                        throw new BGException(ex);
                                    }
                                }
 
                            }
                            , CustomTariffCostChangedEvent.class);
 
                    //При изменении переопределённых цен на договоре обновляем все переопределения для cid
                    EventProcessor.getInstance().addListener(new EventListener<ContractCustomTariffCostsChangedEvent>()
                    {
                        @Override
                        public void notify(ContractCustomTariffCostsChangedEvent e, EventListenerContext ctx)
                                throws BGException
                        {
                            try {
                                CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());
                            } catch (SQLException ex) {
                                throw new BGException(ex);
                            }
                        }
 
                    }
                    , ContractCustomTariffCostsChangedEvent.class);
 
 
                    //При изменении тарифа договора обновляем все переопределения для cid
                    EventProcessor.getInstance().addListener(new EventListener<ContractTariffChangedEvent>()
                            {
                                @Override
                                public void notify(ContractTariffChangedEvent e, EventListenerContext ctx)
                                        throws BGException
                                {
                                    try{
                                        CustomTariffCostCache.this.reloadTariffCostMapForCid(ctx.getConnection(), e.getContractId());
                                    } catch (SQLException ex) {
                                        throw new BGException(ex);
                                    }
                                }
 
                            }
                            , ContractTariffChangedEvent.class);
                }catch (BGException e){
                    CustomTariffCostCache.logger.error(e.getMessage(), e);
                }
            }
        }
        .start();
    }
 
    public static CustomTariffCostCache getInstance(){
        return instance;
    }
 
    /**
     * Возвращаем переопределение цены услуги sid по договору cid на дату date для тарифа с tree_id.
     * Если таких переопределений несколько - возвращаем первое.
     * (Предполагаем, что переопределение только одно в каждый момент времени)
     * @param cid cid
     * @param sid sid
     * @param tree_id id тарифного дерева
     * @param dt дата, на которую должно быть активно переопределение
     * @return объект переопределения цены
     */
    public CustomTariffCost get(Connection con, int cid, int tree_id, int sid, Date dt) throws SQLException {
 
        SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>> cidCostMapRefRef =
                this.tariffCostMap.get(cid);
        AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> cidCostMapRef = null;
        if(cidCostMapRefRef!=null){
            cidCostMapRef = cidCostMapRefRef.get();
        }
        if(cidCostMapRef==null){//нет - значит нужно получить
            cidCostMapRef = this.reloadTariffCostMapForCid(con, cid);
        }
        if(cidCostMapRef==null || cidCostMapRef.get()==null){
            //В кэше ничего нет для этого cid,
            // но есть пустой cidCostMapRef => переопределений цены на договоре нет
            //Либо, если cidCostMapRef==null, то по какой-то причине reloadTariffCostMapForCid не вернул нормального значения (такого не должно быть по идее)
            return null;
        }
        ConcurrentMap<Integer, List<CustomTariffCost>> cidTreeIdCostMap = cidCostMapRef.get().get(tree_id);
        if(cidTreeIdCostMap!=null){
            List<CustomTariffCost> costMapList = cidTreeIdCostMap.get(sid);
            if(costMapList!=null){
                //ищем в списке подходящее по датам переопределение
                for(CustomTariffCost cost : costMapList){
                    if((cost.getDate1()==null || TimeUtils.dateBeforeOrEq(cost.getDate1(),dt))&&
                            (cost.getDate2()==null || TimeUtils.dateBeforeOrEq(dt,cost.getDate2()))){
                        return cost;
                    }
                }
            }
 
        }
 
        return null;
    }
 
    private AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> reloadTariffCostMapForCid(Connection con, int cid) throws SQLException {
        //Берём новую мэпу с костами для cid
        ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> cidCustomCosts = loadCidCustomCosts(con, cid);
        SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>> costMapRefRef = this.tariffCostMap.get(cid);
        if(costMapRefRef==null){
            costMapRefRef = new SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>(new AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>(cidCustomCosts));
            this.tariffCostMap.put(cid,costMapRefRef);
        }else{
            AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>> costMapRef = costMapRefRef.get();
            if(costMapRef!=null){
                costMapRef.set(cidCustomCosts);
            }else{
                costMapRefRef = new SoftReference<AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>>(new AtomicReference<ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>>(cidCustomCosts));
                this.tariffCostMap.put(cid,costMapRefRef);
            }
        }
        return costMapRefRef.get();
    }
 
    private ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> loadCidCustomCosts(Connection con, int cid) throws SQLException {
        ConcurrentMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>> result = null;
        PreparedStatement ps = con.prepareStatement(
            "select " +
                "tp.tree_id, " +
                "ctc.sid, " +
                "ct.id, " +
                "ctc.value, " +
                "ct.date1, " +
                "ct.date2 " +
            "from " +
                "contract_tariff ct left join " +
                "custom_tariff_cost ctc on ct.id=ctc.contract_tariff_id left join " +
                "tariff_plan tp on ct.tpid=tp.id "+
            "where " +
                "ct.cid=? AND " +
                "not ctc.contract_tariff_id is null AND " +
                "not tp.tree_id is null ");
        ps.setInt(1,cid);
        ResultSet rs = ps.executeQuery();
        int tree_id;
        int sid;
        int contract_tariff_id;
        BigDecimal value;
        Date date1;
        Date date2;
        ConcurrentMap<Integer, List<CustomTariffCost>> integerListConcurrentMap;
        List<CustomTariffCost> customTariffCosts;
        while(rs.next()){
            tree_id = rs.getInt(1);
            sid = rs.getInt(2);
            contract_tariff_id = rs.getInt(3);
            value = rs.getBigDecimal(4);
            date1 = TimeUtils.convertSqlDateToDate(rs.getDate(5));
            date2 = TimeUtils.convertSqlDateToDate(rs.getDate(6));
 
            if(null==result){
                result = new ConcurrentHashMap<Integer, ConcurrentMap<Integer, List<CustomTariffCost>>>();
            }
            integerListConcurrentMap = result.get(tree_id);
            if(null==integerListConcurrentMap){
                integerListConcurrentMap = new ConcurrentHashMap<Integer, List<CustomTariffCost>>();
                result.put(tree_id, integerListConcurrentMap);
            }
            customTariffCosts = integerListConcurrentMap.get(sid);
            if(null==customTariffCosts){
                customTariffCosts = new ArrayList<CustomTariffCost>();
                integerListConcurrentMap.put(sid, customTariffCosts);
            }
            customTariffCosts.add(new CustomTariffCost(contract_tariff_id,sid,value,date1, date2));
        }
        rs.close();
        ps.close();
        return result;
    }
}

Альтернативный узел "Стоимость" модуля NPay

Берём стандартный узел "Стоимость" и пишем свой аналогичный, но с добавленной логикой использования кэша переопределений.

Вернее, на самом деле стандартных узлов у Битела 3: дневной, месячный и годовой.

Все их нужно переопределить по-отдельности.

Дневной:

package ru.bitel.bgbilling.modules.npay.tariff.server;
 
import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;
import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.worker.ThreadContext;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;
 
import java.math.BigDecimal;
 
/**
 * Стандартный DayModeCostTariffTreeNode, но с возможностью переопределения цены
 * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache
 */
public class DayModeCustomCostTariffTreeNode extends TariffTreeNode<NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext>{
    /**
     * cost, указанный в тарифе
     */
    private final BigDecimal cost;
    private final int costType;
    //Может ли быть переопределена цена персонально в договоре
    private final boolean isCustomCostAllowed;
    /**
     * cost на конкретном договоре, используемый для рассчетов
     */
    private BigDecimal currentCost;
 
    public DayModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)
    {
        super(id, parameterMap);
 
        this.cost = parameterMap.getBigDecimal("cost", BigDecimal.ZERO);
        this.currentCost = this.cost;
        this.costType = parameterMap.getInt("type", 0);
        this.isCustomCostAllowed = parameterMap.getBoolean("custom", false);
    }
 
    @Override
    protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {
        this.currentCost = this.cost;
        //Определяем, нужно ли менять цену
        if(this.isCustomCostAllowed){
            //Ищем на договоре переопределение цены для услуги
            /*List<TariffTreeNodeHolder> path = treeContext.getPath(new TariffTreeNodeHolder(treeNodeId, -1, null, null));
            for (TariffTreeNodeHolder node : path){
                logger.info(String.valueOf(treeNodeId)+" : "+String.valueOf(node.treeNodeId));
            }*/
 
            try{
                CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());
                if(customTariffCost!=null){
                    this.currentCost=customTariffCost.getValue();
                    logger.debug("custom cost for node_id="+treeNodeId+", cid="+req.cid+", sid="+req.serviceCost.serviceId+", value="+this.currentCost+" (was "+this.cost+")");
                }
            }catch (Exception e){
                logger.error(e.getMessage(), e);
            }
        }
 
        //req.addServiceCost(new NPayTariffRequest.NPayServiceCost(-req.serviceCost.serviceId,));
 
        req.serviceCost.cost = (this.costType == 0 ? this.currentCost : this.currentCost.divide(BigDecimal.valueOf(req.getAccountingMonthDays()), ctx.mc));
        return 1;
    }
}

Месячный:

package ru.bitel.bgbilling.modules.npay.tariff.server;
 
import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;
import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.worker.ThreadContext;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;
 
import java.math.BigDecimal;
 
/**
 * Стандартный MonthModeCostTariffTreeNode, но с возможностью переопределения цены
 * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache
 */
public class MonthModeCustomCostTariffTreeNode extends TariffTreeNode<NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext> {
    /**
     * cost, указанный в тарифе
     */
    private final BigDecimal cost;
    private final int type;
    //Может ли быть переопределена цена персонально в договоре
    private final boolean isCustomCostAllowed;
    /**
     * cost на конкретном договоре, используемый для рассчетов
     */
    private BigDecimal currentCost;
 
    public MonthModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)
    {
        super(id, parameterMap);
 
        this.cost = parameterMap.getBigDecimal("cost", BigDecimal.ZERO);
        this.currentCost = this.cost;
        this.type = parameterMap.getInt("type", 0);
        this.isCustomCostAllowed = parameterMap.getBoolean("custom", false);
    }
 
    @Override
    protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {
        this.currentCost = this.cost;
        //Определяем, нужно ли менять цену
        if(this.isCustomCostAllowed){
            //Ищем на договоре переопределение цены для услуги
            try{
                CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());
                if(customTariffCost!=null){
                    this.currentCost=customTariffCost.getValue();
                    logger.debug("custom cost for node_id="+treeNodeId+", cid="+req.cid+", sid="+req.serviceCost.serviceId+", value="+this.currentCost+" (was "+this.cost+")");
                }
            }catch (Exception e){
                logger.error(e.getMessage(), e);
            }
        }
 
        int monthDays = req.getAccountingMonthDays();
        int periodDays = req.serviceCost.accountingPeriodDays;
 
        BigDecimal monthAmount = req.serviceCost.monthAmount;
        BigDecimal periodAmount = req.serviceCost.periodAmount;
        BigDecimal cost;
        switch (this.type)
        {
            case 0:
                if (periodDays > 0)
                {
                    cost = this.currentCost;
                }
                else
                {
                    cost = BigDecimal.ZERO;
                }
                break;
            case 1:
                cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(
                        BigDecimal.valueOf(monthDays), ctx.mc);
 
                break;
            case 2:
                if ((monthAmount != null) && (periodAmount != null))
                {
                    cost = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);
                }
                else
                {
                    cost = BigDecimal.ZERO;
                }
                break;
            case 3:
                cost = this.currentCost.multiply(BigDecimal.valueOf(periodDays), ctx.mc).divide(BigDecimal.valueOf(monthDays), ctx.mc);
 
                if ((monthAmount != null) && (periodAmount != null))
                {
                    BigDecimal costPropAmount = this.currentCost.multiply(periodAmount, ctx.mc).divide(monthAmount, ctx.mc);
                    if (costPropAmount.compareTo(cost) > 0)
                    {
                        cost = costPropAmount;
                    }
                }
 
                break;
            default:
                cost = BigDecimal.ZERO;
        }
 
        req.serviceCost.cost = cost;
 
        return 1;
    }
}

Годовой:

package ru.bitel.bgbilling.modules.npay.tariff.server;
 
import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;
import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.worker.ThreadContext;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostCache;
 
import java.math.BigDecimal;
 
/**
 * Стандартный YearModeCostTariffTreeNode, но с возможностью переопределения цены
 * Если в узле указано custom=1, то ищем по cid, sid, date новую цену на договоре через CustomTariffCostCache
 */
public class YearModeCustomCostTariffTreeNode extends TariffTreeNode<NPayTariffRequest, NPayTariffContext, TreeContext, ThreadContext> {
    /**
     * cost, указанный в тарифе
     */
    private final BigDecimal cost;
    //Может ли быть переопределена цена персонально в договоре
    private final boolean isCustomCostAllowed;
    /**
     * cost на конкретном договоре, используемый для рассчетов
     */
    private BigDecimal currentCost;
 
    public YearModeCustomCostTariffTreeNode(int id, ParameterMap parameterMap)
    {
        super(id, parameterMap);
 
        this.cost = parameterMap.getBigDecimal("cost", BigDecimal.ZERO);
        this.currentCost = this.cost;
        this.isCustomCostAllowed = parameterMap.getBoolean("custom", false);
    }
 
    protected int executeImpl(Long treeNodeId, Long parentTreeNodeId, NPayTariffRequest req, NPayTariffContext ctx, TreeContext treeContext, ThreadContext workerContext) {
        this.currentCost = this.cost;
        //Определяем, нужно ли менять цену
        if(this.isCustomCostAllowed){
            //Ищем на договоре переопределение цены для услуги
            try{
                CustomTariffCost customTariffCost = CustomTariffCostCache.getInstance().get(req.getConnection(), req.cid, treeContext.getTariffModuleTreeId(), req.serviceCost.serviceId,req.getTime().getTime());
                if(customTariffCost!=null){
                    this.currentCost=customTariffCost.getValue();
                    logger.debug("custom cost for node_id="+treeNodeId+", cid="+req.cid+", sid="+req.serviceCost.serviceId+", value="+this.currentCost+" (was "+this.cost+")");
                }
            }catch (Exception e){
                logger.error(e.getMessage(), e);
            }
        }
 
        req.serviceCost.cost = this.currentCost;
        return 1;
    }
}

Теперь заменим стандартные узлы тарифов абонплат нашими самописными. Для этого нужно распаковать из npay.jar файл bitel/billing/common/tariff/npay.xml и заменить в нём определения day_cost, month_cost и year_cost :

<node type="day_cost" class1="bitel.billing.module.services.npay.DayModeCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCostTariffTreeNode"/>
...
<node type="month_cost" class1="bitel.billing.module.services.npay.MonthModeCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCostTariffTreeNode"/> 
... 
<node type="year_cost" class1="bitel.billing.module.services.npay.YearModeCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCostTariffTreeNode"/>

На наши:

<node type="day_cost" class1="bitel.billing.module.services.npay.DayModeCustomCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.DayModeCustomCostTariffTreeNode"/>
<node type="month_cost" class1="bitel.billing.module.services.npay.MonthModeCustomCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.MonthModeCustomCostTariffTreeNode"/>
<node type="year_cost" class1="bitel.billing.module.services.npay.YearModeCustomCostTariffTreeNode" title="Стоимость" class2="ru.bitel.bgbilling.modules.npay.tariff.server.YearModeCustomCostTariffTreeNode"/>

Как это сделать?

С одной стороны, можно внести изменения в npay.xml и запаковать его обратно в npay.xml. Но тогда это придётся делать при каждом обновлении. С другой стороны, я просто положил это файл в /usr/local/BGBillingServer/bitel/billing/common/tariff на сервере и перезапустил сервисы - всё заработало: файл загружается приоритетнее стандартного из npay.jar

Но нужно понимать, что при любом обновлении нужно будет следить за изменениями стандартного Npay.xml и соответствующим образом менять собственный файл.

Интерфейс

Редактирование цен

Отлично, теперь цены у нас переопределяются по табличке custom_tariff_cost. Теперь нужно написать удобный интерфейс работы с этой табличкой.

Веб-сервис переопределения цен

Вспомогательные классы:

package ru.dsi.bgbilling.kernel.discount.api.common.bean;
 
import ru.bitel.common.model.Id;
 
import java.io.Serializable;
import java.util.Date;
 
/**
 * @author cromeshnic@gmail.com
 * Тарифный план договора для использования в веб-сервисе.
 * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем делать так у себя
 *
 */
public class ContractTariff extends Id implements Serializable {
    private int contractId;
    private int tariffPlanId;
    private Date date1;
    private Date date2;
    private String comment = "";
    private String tariffName;
    private int pos;
    private int entityMid;
    private int entityId;
    private int replacedFromContractTariffId;
 
    public ContractTariff() {
        this.id = -1;
        this.contractId = -1;
        this.tariffPlanId = -1;
        this.date1 = null;
        this.date2 = null;
        this.tariffName = null;
        this.pos = 0;
        this.entityId = 0;
        this.entityMid = 0;
        this.replacedFromContractTariffId = -1;
    }
 
    public int getContractId() {
        return contractId;
    }
 
    public void setContractId(int contractId) {
        this.contractId = contractId;
    }
 
    public int getTariffPlanId() {
        return tariffPlanId;
    }
 
    public String getTariffName() {
        return tariffName;
    }
 
    public void setTariffName(String tariffName) {
        this.tariffName = tariffName;
    }
 
    public void setTariffPlanId(int tariffPlanId) {
        this.tariffPlanId = tariffPlanId;
    }
 
    public Date getDate1() {
        return date1;
    }
 
    public void setDate1(Date date1) {
        this.date1 = date1;
    }
 
    public Date getDate2() {
        return date2;
    }
 
    public void setDate2(Date date2) {
        this.date2 = date2;
    }
 
    public String getComment() {
        return comment;
    }
 
    public void setComment(String comment) {
        this.comment = comment;
    }
 
    public int getPos() {
        return pos;
    }
 
    public void setPos(int pos) {
        this.pos = pos;
    }
 
    public int getEntityMid() {
        return entityMid;
    }
 
    public void setEntityMid(int entityMid) {
        this.entityMid = entityMid;
    }
 
    public int getEntityId() {
        return entityId;
    }
 
    public void setEntityId(int entityId) {
        this.entityId = entityId;
    }
 
    public int getReplacedFromContractTariffId() {
        return replacedFromContractTariffId;
    }
 
    public void setReplacedFromContractTariffId(int replacedFromContractTariffId) {
        this.replacedFromContractTariffId = replacedFromContractTariffId;
    }
}
package ru.bitel.bgbilling.kernel.discount.api.common.service;
 
import ru.bitel.bgbilling.common.BGException;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;
 
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlSeeAlso;
import java.util.List;
 
/**
 * @author cromeshnic@gmail.com
 * Пока бител не перевёл стандартный экшн получения списка тарифов догвора на вебэкшен, будем пользоваться собственным сервисом
 *
 */
@WebService
@XmlSeeAlso({ContractTariff.class})
public abstract interface ContractTariffService {
    @WebMethod
    public abstract List<ContractTariff> contractTariffList(@WebParam(name="contractId") int contracId) throws BGException;
}
package ru.bitel.bgbilling.kernel.discount.api.server.service;
 
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;
import ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.ContractTariff;
import ru.dsi.bgbilling.kernel.discount.bean.ContractTariffDao;
 
import javax.jws.WebParam;
import javax.jws.WebService;
import java.util.List;
 
/**
 * @see ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService
 */
@WebService(endpointInterface="ru.bitel.bgbilling.kernel.discount.api.common.service.ContractTariffService")
public class ContractTariffServiceImpl extends AbstractService
        implements ContractTariffService {
 
    @Override
    public List<ContractTariff> contractTariffList(@WebParam(name = "contractId") int contracId) throws BGException {
        return (new ContractTariffDao(getConnection()).list(contracId));
    }
}

Веб-сервис работы с переопределёнными ценами:

package ru.bitel.bgbilling.kernel.discount.api.common.service;
 
import ru.bitel.bgbilling.common.BGException;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
 
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlSeeAlso;
import java.util.List;
import java.util.Set;
 
/**
 * @author cromeshnic@gmail.com
 * Интерфейс веб-сервиса для работы с персональными ценами
 */
@WebService
@XmlSeeAlso({CustomTariffCost.class})
public abstract interface CustomTariffCostService {
 
    @WebMethod
    public abstract List<CustomTariffCost> customTariffCostList(@WebParam(name="contractTariffId") int contract_tariff_id)
            throws BGException;
 
    @WebMethod
    public abstract void updateCustomTariffCostList(@WebParam(name="contractTariffId") int contract_tariff_id, @WebParam(name="costs") List<CustomTariffCost> costs)
            throws BGException;
 
    /**
     * Возвращает список id услуг, для которых на этом тарифном плане разрешено переопределять цену
     */
    @WebMethod
    public abstract Set<Integer> availableServiceList(@WebParam(name="tariffPlanId") int tpid) throws BGException;
}
package ru.bitel.bgbilling.kernel.discount.api.server.service;
 
import bitel.billing.server.contract.bean.ContractTariff;
import bitel.billing.server.contract.bean.ContractTariffManager;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.service.server.AbstractService;
import ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService;
import ru.bitel.bgbilling.kernel.tariff.common.bean.TariffLabelItem;
import ru.bitel.bgbilling.kernel.tariff.server.bean.TariffLabelManager;
import ru.bitel.bgbilling.server.util.ClosedDateChecker;
import ru.dsi.bgbilling.kernel.discount.api.common.bean.CustomTariffCost;
import ru.dsi.bgbilling.kernel.discount.bean.CustomTariffCostDao;
 
import javax.jws.WebParam;
import javax.jws.WebService;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * Реализация веб-сервиса для работы с персональными ценам
 */
@WebService(endpointInterface="ru.bitel.bgbilling.kernel.discount.api.common.service.CustomTariffCostService")
public class CustomTariffCostServiceImpl extends AbstractService
        implements CustomTariffCostService {
 
    @Override
    public List<CustomTariffCost> customTariffCostList(@WebParam(name = "contractTariffId") int contract_tariff_id) throws BGException {
        return (new CustomTariffCostDao(getConnection())).getContractTariffCosts(contract_tariff_id);
    }
 
    @Override
    public void updateCustomTariffCostList(@WebParam(name = "contractTariffId")int contract_tariff_id, @WebParam(name="costs") List<CustomTariffCost> costs) throws BGException{
 
        Calendar closedDate;
        ContractTariffManager ctm = new ContractTariffManager(getConnection());
        ContractTariff ct = ctm.getContractTariffById(contract_tariff_id);
        if ((closedDate = ClosedDateChecker.getClosePeriodDateIfChecking("updateCustomTariffCostList", 0, this.userId)) != null)
        {
            ClosedDateChecker.checkDatesForUpdate(closedDate, null, null, ct.getDate1(), ct.getDate2());
        }
        CustomTariffCostDao ctcDao = new CustomTariffCostDao(getConnection());
        try {
            ctcDao.updateContractCustomTariffCosts(ct, costs, this.userId);
        } catch (SQLException e) {
            throw new BGException(e);
        }
    }
 
    /**
     * Возвращает список услуг, для которых на этом тарифном плане разрешено переопределять цену
     * На данный момент реализовано через метки - ищем все метки тарифного плана вида "id - Название",
     * где id - это id услуги. Например "5 - Интернет".
     */
    @Override
    public Set<Integer> availableServiceList(@WebParam(name = "tariffPlanId") int tpid)
            throws BGException
    {
        Set<Integer> result = new TreeSet<Integer>();
        TariffLabelManager tlm = new TariffLabelManager(getConnection());
        //Получаем список id меток тарифа
        Set<Integer> tariffLabelIds = tlm.getTariffLabelIds(tpid);
        if(tariffLabelIds.size()>0){
            //Берём список всех меток с названиями
            List<TariffLabelItem> tariffLabelItemList = tlm.getTariffLabelItemList();
            //Ищем среди меток тарифа метки с названиями вида "id - Название"
            /*for(Integer label_id : tariffLabelIds){
                TariffLabelItem label = tariffLabelItemList.get()
            }*/
            Pattern pattern = Pattern.compile("^(\\d+) - .+$");
 
            Matcher matcher;
            //Перебираем все метки из справочника
            for(TariffLabelItem label : tariffLabelItemList){
                //Если метка есть на нашем тарифе ...
                if(tariffLabelIds.contains(label.getId())){
                    //...то смотрим её название
                    matcher = pattern.matcher(label.getTitle());
                    if(matcher.find()){//определился sid услуги - отлично, добавляем в результат
                        try{
                            result.add(Integer.valueOf(matcher.group(1)));
                        }catch (Exception e){}
                    }
                }
            }
        }
        return result;
    }
 
}

Клиетский интерфейс

Для начала немного костылей.

Поскольку услуг в биллинге заведено много, а в конкретном тарифе обычно можно переопределять всего несколько, то для облегчения интерфейса веб-сервис выдаёт только те услуги, которые разрешено переопределять для данного тарифа. Но получать список таких услуг через разбор тарифного дерева представляется излишне сложной задачей, поэтому было решено реализовать список разрешённых к переопределению услуг через тарифные метки:

Файл:custom_cost_labels.jpg

Отображение на договоре

Заключение

Личные инструменты